Interactor AI Agents Skill
Build LLM-powered assistants that can have conversations, use tools, and access your data sources.
When to Use
- Conversational AI: Building chat interfaces with AI assistants
- Tool-Calling Agents: Creating assistants that can invoke custom functions
- Data-Connected AI: Connecting AI to databases for natural language queries
- Customer Support Bots: Building support assistants with domain knowledge
- Internal Tools: Creating AI assistants for internal operations
Prerequisites
- Interactor authentication configured (see
interactor-authskill) - Understanding of LLM concepts (prompts, tools, context)
- Webhook endpoint for tool callbacks (optional, for custom tools)
- Database with network access from Interactor (optional, for data sources)
jqcommand-line tool (for bash examples)
Overview
The AI Agents system consists of:
| Component | Description |
|---|---|
| Assistants | Configured AI agents with specific behaviors and capabilities |
| Rooms | Chat sessions between users and assistants |
| Messages | Individual messages in a conversation |
| Tools | Custom functions that assistants can invoke |
| Data Sources | Databases and APIs that assistants can query |
Quick Start
The typical implementation flow:
1. Create an Assistant → Define behavior with instructions and model config
↓
2. Register Tools → (Optional) Add custom functions the assistant can call
↓
3. Connect Data Sources → (Optional) Connect databases for natural language queries
↓
4. Create a Room → Start a conversation session for a user
↓
5. Send/Receive Messages → Exchange messages, handle tool calls
↓
6. Close Room → End the conversation when complete
Minimal Example:
Prerequisites: This example requires
jqfor JSON parsing and a valid$TOKEN. Seeinteractor-authskill for authentication setup.
# Get your token first (see interactor-auth skill)
# export TOKEN="your_access_token_here"
# 1. Create assistant
ASSISTANT_ID=$(curl -s -X POST https://core.interactor.com/api/v1/agents/assistants \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "helper", "title": "Helper", "instructions": "You are a helpful assistant."}' \
| jq -r '.data.id')
echo "Created assistant: $ASSISTANT_ID"
# 2. Create room
ROOM_ID=$(curl -s -X POST https://core.interactor.com/api/v1/agents/$ASSISTANT_ID/rooms \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"namespace": "user_123"}' \
| jq -r '.data.id')
echo "Created room: $ROOM_ID"
# 3. Send message
curl -X POST https://core.interactor.com/api/v1/agents/rooms/$ROOM_ID/messages \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"content": "Hello!", "role": "user"}'
Instructions
API Version Note: This skill documents the standard AI Agents API. Some operations (DELETE endpoints, advanced pagination options) may vary by Interactor version. Always verify endpoint availability against your specific API documentation or test in a development environment first.
Step 1: Create an Assistant
curl -X POST https://core.interactor.com/api/v1/agents/assistants \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "support_assistant",
"title": "Support Assistant",
"description": "Helps users with support questions",
"model_config": {
"provider": "openai",
"model": "gpt-4o",
"temperature": 0.7
},
"instructions": "You are a helpful support assistant. Be concise and friendly. Always try to resolve issues on the first response.",
"enabled_tools": ["search_knowledge_base", "create_ticket"]
}'
Response:
{
"data": {
"id": "asst_abc",
"name": "support_assistant",
"title": "Support Assistant",
"description": "Helps users with support questions",
"model_config": {
"provider": "openai",
"model": "gpt-4o",
"temperature": 0.7
},
"enabled_tools": ["search_knowledge_base", "create_ticket"],
"created_at": "2026-01-20T12:00:00Z"
}
}
Assistant Configuration Options
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Unique identifier (lowercase, underscores only) |
title |
string | Yes | Display name for users |
description |
string | No | What the assistant does |
model_config.provider |
string | No | openai (default: openai) |
model_config.model |
string | No | Model identifier (default: gpt-4o) |
model_config.temperature |
number | No | Response randomness 0.0-1.0 (default: 0.7) |
instructions |
string | Yes | System prompt defining behavior |
enabled_tools |
array | No | Tool names the assistant can use |
List Assistants
curl https://core.interactor.com/api/v1/agents/assistants \
-H "Authorization: Bearer <token>"
Get Assistant
curl https://core.interactor.com/api/v1/agents/assistants/asst_abc \
-H "Authorization: Bearer <token>"
Update Assistant
curl -X PUT https://core.interactor.com/api/v1/agents/assistants/asst_abc \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"instructions": "Updated instructions with more detail...",
"model_config": {
"temperature": 0.5
}
}'
Delete Assistant
curl -X DELETE https://core.interactor.com/api/v1/agents/assistants/asst_abc \
-H "Authorization: Bearer <token>"
Chat Rooms
Rooms are conversations between a user and an assistant.
Create a Room
curl -X POST https://core.interactor.com/api/v1/agents/asst_abc/rooms \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"namespace": "user_123",
"metadata": {
"user_name": "John",
"context": "billing_question",
"plan": "premium"
}
}'
Response:
{
"data": {
"id": "room_xyz",
"assistant_id": "asst_abc",
"namespace": "user_123",
"status": "active",
"metadata": {
"user_name": "John",
"context": "billing_question",
"plan": "premium"
},
"created_at": "2026-01-20T12:00:00Z"
}
}
List Rooms
curl https://core.interactor.com/api/v1/agents/rooms \
-H "Authorization: Bearer <token>"
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
namespace |
string | Filter by namespace |
assistant_id |
string | Filter by assistant |
status |
string | active or closed |
Example - List active rooms for a user:
curl "https://core.interactor.com/api/v1/agents/rooms?namespace=user_123&status=active" \
-H "Authorization: Bearer <token>"
Get Room
curl https://core.interactor.com/api/v1/agents/rooms/room_xyz \
-H "Authorization: Bearer <token>"
Response includes conversation history:
{
"data": {
"id": "room_xyz",
"assistant_id": "asst_abc",
"status": "active",
"metadata": {...},
"message_count": 5,
"created_at": "2026-01-20T12:00:00Z",
"last_message_at": "2026-01-20T12:05:00Z"
}
}
Close Room
curl -X POST https://core.interactor.com/api/v1/agents/rooms/room_xyz/close \
-H "Authorization: Bearer <token>"
Messages
Send a Message
curl -X POST https://core.interactor.com/api/v1/agents/rooms/room_xyz/messages \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"content": "How do I update my billing information?",
"role": "user"
}'
Response:
{
"data": {
"id": "msg_123",
"role": "user",
"content": "How do I update my billing information?",
"created_at": "2026-01-20T12:00:00Z"
}
}
The assistant's response is generated asynchronously. Use streaming or webhooks to receive it.
List Messages
curl https://core.interactor.com/api/v1/agents/rooms/room_xyz/messages \
-H "Authorization: Bearer <token>"
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
integer | Max messages to return (default: 50, max: 100) |
cursor |
string | Opaque cursor for pagination (from next_cursor) |
order |
string | Sort order: asc (oldest first) or desc (newest first, default) |
Response:
{
"data": {
"messages": [
{
"id": "msg_123",
"role": "user",
"content": "How do I update my billing information?",
"created_at": "2026-01-20T12:00:00Z"
},
{
"id": "msg_124",
"role": "assistant",
"content": "To update your billing information, go to Settings > Billing...",
"tool_calls": [],
"created_at": "2026-01-20T12:00:05Z"
}
],
"has_more": true,
"next_cursor": "eyJpZCI6Im1zZ18xMjQiLCJ0cyI6MTcwNTc1MjQwNX0="
}
}
Pagination Pattern
The API uses cursor-based pagination for stable, consistent results:
// Fetch all messages in a room
async function getAllMessages(roomId: string): Promise<Message[]> {
const allMessages: Message[] = [];
let cursor: string | undefined;
do {
const params = new URLSearchParams({ limit: '100' });
if (cursor) params.set('cursor', cursor);
const response = await fetch(
`https://core.interactor.com/api/v1/agents/rooms/${roomId}/messages?${params}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
const { data } = await response.json();
allMessages.push(...data.messages);
cursor = data.has_more ? data.next_cursor : undefined;
} while (cursor);
return allMessages;
}
Pagination Best Practices:
- Always use
next_cursorfrom the response; never construct cursors manually - Cursors are opaque and may change format between API versions
- Cursors expire after 24 hours; for long-running jobs, re-fetch from the beginning
- Results are stable within a cursor session (new messages won't appear mid-pagination)
Message Roles
| Role | Description |
|---|---|
user |
Message from the user |
assistant |
Response from the AI assistant |
system |
System message (internal use) |
tool |
Tool execution result |
Real-Time Responses
Assistant responses are generated asynchronously after you send a user message. To receive responses in real-time, you have three options:
API Note: Verify streaming endpoint availability with your Interactor version. The SSE endpoint pattern shown below (
/rooms/{id}/stream) is a common convention but may differ in your deployment. Check your API documentation or contact support.
Option 1: Server-Sent Events (SSE)
Subscribe to room events for streaming tokens as they're generated.
Note: The standard browser
EventSourceAPI doesn't support custom headers. Use theeventsourcenpm package for Node.js, or pass the token as a query parameter if your API supports it.
Node.js with eventsource package:
import EventSource from 'eventsource';
// Node.js: Use eventsource package which supports headers
const eventSource = new EventSource(
`https://core.interactor.com/api/v1/agents/rooms/${roomId}/stream`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'token') {
// Append token to response
process.stdout.write(data.token);
} else if (data.type === 'message_complete') {
// Full message available
console.log('\nComplete:', data.message);
} else if (data.type === 'tool_call') {
// Tool is being invoked
console.log('Tool call:', data.tool_name);
}
};
eventSource.onerror = (error) => {
console.error('Stream error:', error);
eventSource.close();
};
Browser with Fetch API (alternative):
// Browser: Use fetch with ReadableStream for SSE with auth
async function streamMessages(roomId: string, token: string) {
const response = await fetch(
`https://core.interactor.com/api/v1/agents/rooms/${roomId}/stream`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (reader) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// Parse SSE format: "data: {...}\n\n"
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
console.log('Received:', data);
}
}
}
}
Option 2: Webhooks
Configure a webhook to receive agent.room.message events at your endpoint. See the interactor-webhooks skill for complete webhook setup and payload handling.
Option 3: Polling (Not Recommended)
Poll the messages endpoint for new messages. Use only as a fallback:
async function pollForResponse(roomId: string, lastKnownMessageId: string) {
const maxAttempts = 30;
const delayMs = 1000;
for (let i = 0; i < maxAttempts; i++) {
// Fetch recent messages and check for new assistant response
const { messages } = await getMessages(roomId, { limit: 10 });
const newMessages = messages.filter(m =>
m.role === 'assistant' && m.id !== lastKnownMessageId
);
if (newMessages.length > 0) return newMessages[0];
await new Promise(resolve => setTimeout(resolve, delayMs));
}
throw new Error('Response timeout');
}
Tools
Tools are custom functions that assistants can invoke during conversations.
Register a Tool
curl -X POST https://core.interactor.com/api/v1/tools \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "search_products",
"description": "Search the product catalog by query and optional category",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query for product names or descriptions"
},
"category": {
"type": "string",
"description": "Product category to filter by",
"enum": ["electronics", "clothing", "home", "sports"]
},
"max_results": {
"type": "integer",
"description": "Maximum number of results to return",
"default": 10
}
},
"required": ["query"]
},
"callback_url": "https://yourapp.com/api/tools/search_products",
"callback_secret": "your_webhook_secret_here"
}'
Response:
{
"data": {
"id": "tool_abc",
"name": "search_products",
"description": "Search the product catalog by query and optional category",
"created_at": "2026-01-20T12:00:00Z"
}
}
Tool Callback
When the assistant invokes your tool, Interactor POSTs to your callback_url:
Request Headers:
POST /api/tools/search_products HTTP/1.1
Content-Type: application/json
X-Interactor-Signature: sha256=abc123def456...
X-Interactor-Timestamp: 2026-01-20T12:00:00Z
Request Body:
{
"tool_name": "search_products",
"parameters": {
"query": "laptop",
"category": "electronics",
"max_results": 5
},
"execution_id": "exec_xyz",
"room_id": "room_xyz",
"assistant_id": "asst_abc",
"timestamp": "2026-01-20T12:00:00Z"
}
Security Note: The
X-Interactor-Signatureheader contains an HMAC-SHA256 signature of the raw request body, signed with yourcallback_secret. Always verify this signature before processing the request.
Your response:
{
"result": {
"products": [
{
"id": "prod_1",
"name": "MacBook Pro 14\"",
"price": 1999,
"in_stock": true
},
{
"id": "prod_2",
"name": "Dell XPS 15",
"price": 1499,
"in_stock": true
}
],
"total_count": 2
}
}
The assistant will use this result to formulate its response.
Verify Tool Callback Signature
Tool callbacks include three security headers for verification:
| Header | Description |
|---|---|
X-Interactor-Signature |
HMAC-SHA256 signature of the raw request body |
X-Interactor-Timestamp |
ISO 8601 timestamp when request was signed |
X-Interactor-Request-Id |
Unique request ID (same as execution_id in body) |
Security Requirements:
- Verify HMAC signature matches
- Check timestamp is within acceptable window (recommended: 300 seconds)
- Use
execution_idas idempotency key to prevent replay attacks
TypeScript (with replay protection):
import crypto from 'crypto';
interface VerificationResult {
valid: boolean;
error?: string;
}
function verifyToolCallback(
payload: string,
signature: string,
timestamp: string,
secret: string,
maxAgeSeconds: number = 300
): VerificationResult {
// 1. Check timestamp is not too old (replay protection)
const requestTime = Date.parse(timestamp);
const now = Date.now();
const ageSeconds = Math.abs(now - requestTime) / 1000;
if (ageSeconds > maxAgeSeconds) {
return { valid: false, error: `Request too old: ${ageSeconds}s > ${maxAgeSeconds}s` };
}
// 2. Verify HMAC signature
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
const signatureValid = crypto.timingSafeEqual(
Buffer.from(signature.replace('sha256=', '')),
Buffer.from(expected)
);
if (!signatureValid) {
return { valid: false, error: 'Invalid signature' };
}
return { valid: true };
}
// Express middleware with full security
app.post('/api/tools/search_products', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.headers['x-interactor-signature'] as string;
const timestamp = req.headers['x-interactor-timestamp'] as string;
const requestId = req.headers['x-interactor-request-id'] as string;
const payload = req.body.toString();
// Verify signature and timestamp
const verification = verifyToolCallback(
payload,
signature,
timestamp,
process.env.TOOL_CALLBACK_SECRET!
);
if (!verification.valid) {
console.error('Callback verification failed:', verification.error, { requestId });
return res.status(401).json({ error: verification.error });
}
const data = JSON.parse(payload);
// 3. Idempotency check - prevent duplicate processing
const alreadyProcessed = await checkIdempotency(data.execution_id);
if (alreadyProcessed) {
console.log('Duplicate request ignored:', data.execution_id);
return res.status(200).json({ result: await getCachedResult(data.execution_id) });
}
// Execute your tool logic
const result = await searchProducts(data.parameters);
// Store result for idempotency (retain for 7 days)
await storeIdempotencyResult(data.execution_id, result, 7 * 24 * 60 * 60);
res.json({ result });
});
// Idempotency helpers (implement with Redis, database, etc.)
async function checkIdempotency(executionId: string): Promise<boolean> {
// Check if execution_id was already processed
return false; // Implement with your storage
}
async function getCachedResult(executionId: string): Promise<any> {
// Return cached result for duplicate request
return null; // Implement with your storage
}
async function storeIdempotencyResult(executionId: string, result: any, ttlSeconds: number): Promise<void> {
// Store result keyed by execution_id
// Implement with your storage
}
Python (with replay protection):
import os
import hmac
import hashlib
import time
from datetime import datetime
from flask import Flask, request, jsonify
app = Flask(__name__)
def verify_tool_callback(
payload: bytes,
signature: str,
timestamp: str,
secret: str,
max_age_seconds: int = 300
) -> tuple[bool, str | None]:
"""
Verify callback with replay protection.
Returns (is_valid, error_message).
"""
# 1. Check timestamp is not too old
try:
request_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
age_seconds = abs((datetime.now(request_time.tzinfo) - request_time).total_seconds())
if age_seconds > max_age_seconds:
return False, f'Request too old: {age_seconds}s > {max_age_seconds}s'
except ValueError as e:
return False, f'Invalid timestamp: {e}'
# 2. Verify HMAC signature
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
signature_value = signature.replace('sha256=', '')
if not hmac.compare_digest(signature_value, expected):
return False, 'Invalid signature'
return True, None
@app.route('/api/tools/search_products', methods=['POST'])
def handle_tool_callback():
signature = request.headers.get('X-Interactor-Signature', '')
timestamp = request.headers.get('X-Interactor-Timestamp', '')
request_id = request.headers.get('X-Interactor-Request-Id', '')
payload = request.get_data()
# Verify signature and timestamp
is_valid, error = verify_tool_callback(
payload,
signature,
timestamp,
os.environ['TOOL_CALLBACK_SECRET']
)
if not is_valid:
app.logger.error(f'Callback verification failed: {error}', extra={'request_id': request_id})
return jsonify({'error': error}), 401
data = request.get_json()
# Idempotency check
if is_duplicate_execution(data['execution_id']):
return jsonify({'result': get_cached_result(data['execution_id'])}), 200
# Execute your tool logic
result = search_products(data['parameters'])
# Store for idempotency
store_execution_result(data['execution_id'], result)
return jsonify({'result': result})
List Tools
curl https://core.interactor.com/api/v1/tools \
-H "Authorization: Bearer <token>"
Get Tool
curl https://core.interactor.com/api/v1/tools/tool_abc \
-H "Authorization: Bearer <token>"
Update Tool
curl -X PUT https://core.interactor.com/api/v1/tools/tool_abc \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"description": "Updated description with more detail",
"callback_url": "https://yourapp.com/api/v2/tools/search_products"
}'
Delete Tool
curl -X DELETE https://core.interactor.com/api/v1/tools/tool_abc \
-H "Authorization: Bearer <token>"
Webhook & Callback Contract
This section defines the canonical contract for all Interactor webhooks and tool callbacks.
Request Headers
All webhook/callback requests include these headers:
| Header | Format | Description |
|---|---|---|
X-Interactor-Signature |
sha256=<hex> |
HMAC-SHA256 of raw body |
X-Interactor-Timestamp |
ISO 8601 | When request was signed |
X-Interactor-Request-Id |
exec_<id> |
Unique request identifier |
Content-Type |
application/json |
Always JSON |
Response Requirements
| Status | Meaning | Interactor Behavior |
|---|---|---|
200 |
Success | Process result, no retry |
201 |
Created | Same as 200 |
202 |
Accepted | Async processing (see below) |
400 |
Bad Request | Permanent failure, no retry |
401 |
Unauthorized | Permanent failure, no retry |
404 |
Not Found | Permanent failure, no retry |
429 |
Rate Limited | Retry with backoff |
500 |
Server Error | Retry with backoff |
502-504 |
Gateway Error | Retry with backoff |
Expected Response Format:
{
"result": { ... }
}
Timeout Policy
| Operation | Timeout | Behavior on Timeout |
|---|---|---|
| Tool callback | 30 seconds | Retry with backoff |
| Webhook delivery | 10 seconds | Retry with backoff |
| Async callback (202) | 5 minutes | Poll for completion |
Important: Your callback must respond within the timeout window. For long-running operations, return
202 Acceptedwith a status URL, and Interactor will poll for completion.
Retry Policy
When a callback fails (5xx, timeout, network error), Interactor retries with exponential backoff:
| Attempt | Delay | Cumulative Time |
|---|---|---|
| 1 | Immediate | 0s |
| 2 | 1s | 1s |
| 3 | 2s | 3s |
| 4 | 4s | 7s |
| 5 | 8s | 15s |
| 6 (final) | 16s | 31s |
After 6 failed attempts:
- Request is marked as permanently failed
tool.callback.failedevent is emitted (if webhooks configured)- Assistant receives an error and may retry or inform the user
Idempotency Requirements
You MUST implement idempotent handlers. Use execution_id as the idempotency key.
Idempotency Implementation:
// Redis-based idempotency example
import Redis from 'ioredis';
const redis = new Redis();
const IDEMPOTENCY_TTL = 7 * 24 * 60 * 60; // 7 days
async function handleWithIdempotency(
executionId: string,
handler: () => Promise<any>
): Promise<{ result: any; isDuplicate: boolean }> {
const cacheKey = `idempotency:${executionId}`;
// Check for existing result
const cached = await redis.get(cacheKey);
if (cached) {
return { result: JSON.parse(cached), isDuplicate: true };
}
// Execute handler
const result = await handler();
// Store result with TTL
await redis.setex(cacheKey, IDEMPOTENCY_TTL, JSON.stringify(result));
return { result, isDuplicate: false };
}
Recommended idempotency window: 7 days minimum. This covers retry scenarios and delayed processing.
Dead Letter Handling
If all retries are exhausted:
- Interactor emits a
tool.callback.dead_letterevent (if webhooks configured) - The failed request is logged in your Interactor dashboard
- You can manually retry from the dashboard or via API
Dead Letter Event Payload:
{
"event": "tool.callback.dead_letter",
"data": {
"execution_id": "exec_xyz",
"tool_name": "search_products",
"assistant_id": "asst_abc",
"room_id": "room_xyz",
"attempts": 6,
"last_error": "Connection timeout",
"first_attempt_at": "2026-01-20T12:00:00Z",
"last_attempt_at": "2026-01-20T12:00:31Z"
}
}
Streaming Contract
This section defines the canonical contract for Server-Sent Events (SSE) streaming.
Event Types
The streaming endpoint emits these event types:
| Event Type | Description | When Emitted |
|---|---|---|
token |
Incremental response token | During response generation |
message_complete |
Full message available | When response finishes |
tool_call |
Tool is being invoked | When assistant calls a tool |
tool_result |
Tool execution completed | After tool callback returns |
error |
Error occurred | On any error |
done |
Stream complete | End of stream |
Event Schemas
token Event:
{
"type": "token",
"token": "Hello",
"index": 0,
"message_id": "msg_123"
}
| Field | Type | Description |
|---|---|---|
type |
string | Always "token" |
token |
string | The text token (1-4 characters typically) |
index |
integer | Token position in the response |
message_id |
string | ID of the message being generated |
message_complete Event:
{
"type": "message_complete",
"message": {
"id": "msg_123",
"role": "assistant",
"content": "Hello! How can I help you today?",
"tool_calls": [],
"created_at": "2026-01-20T12:00:05Z"
}
}
tool_call Event:
{
"type": "tool_call",
"tool_call": {
"id": "tc_456",
"tool_name": "search_products",
"parameters": {
"query": "laptop"
},
"status": "pending"
},
"message_id": "msg_123"
}
tool_result Event:
{
"type": "tool_result",
"tool_call": {
"id": "tc_456",
"tool_name": "search_products",
"status": "completed",
"result": {
"products": [...]
}
},
"message_id": "msg_123"
}
error Event:
{
"type": "error",
"error": {
"code": "tool_callback_failed",
"message": "Tool callback timed out",
"tool_call_id": "tc_456"
},
"message_id": "msg_123"
}
done Event:
{
"type": "done",
"message_id": "msg_123"
}
SSE Wire Format
Events follow the standard SSE format:
event: token
data: {"type":"token","token":"Hello","index":0,"message_id":"msg_123"}
event: token
data: {"type":"token","token":" world","index":1,"message_id":"msg_123"}
event: message_complete
data: {"type":"message_complete","message":{...}}
event: done
data: {"type":"done","message_id":"msg_123"}
Important: Each event has an event: line followed by a data: line, then a blank line.
Authentication for Streaming
The standard browser EventSource API does not support custom headers. Use one of these approaches:
Option 1: Short-Lived Token in Query Parameter (Recommended for Browsers)
// 1. Get a short-lived streaming token (valid 60 seconds)
const { streaming_token } = await fetch('/api/v1/agents/rooms/{room_id}/stream-token', {
method: 'POST',
headers: { 'Authorization': `Bearer ${accessToken}` }
}).then(r => r.json());
// 2. Connect with token in query string
const eventSource = new EventSource(
`https://core.interactor.com/api/v1/agents/rooms/${roomId}/stream?token=${streaming_token}`
);
Option 2: Fetch API with ReadableStream
async function streamWithAuth(roomId: string, accessToken: string) {
const response = await fetch(
`https://core.interactor.com/api/v1/agents/rooms/${roomId}/stream`,
{
headers: { 'Authorization': `Bearer ${accessToken}` }
}
);
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (reader) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// Parse complete events from buffer
const events = buffer.split('\n\n');
buffer = events.pop() || ''; // Keep incomplete event in buffer
for (const eventBlock of events) {
if (!eventBlock.trim()) continue;
const lines = eventBlock.split('\n');
let eventType = 'message';
let data = '';
for (const line of lines) {
if (line.startsWith('event: ')) {
eventType = line.slice(7);
} else if (line.startsWith('data: ')) {
data = line.slice(6);
}
}
if (data) {
const parsed = JSON.parse(data);
handleStreamEvent(eventType, parsed);
}
}
}
}
function handleStreamEvent(eventType: string, data: any) {
switch (data.type) {
case 'token':
process.stdout.write(data.token);
break;
case 'message_complete':
console.log('\n[Complete]', data.message.content);
break;
case 'tool_call':
console.log('[Tool]', data.tool_call.tool_name);
break;
case 'error':
console.error('[Error]', data.error.message);
break;
case 'done':
console.log('[Stream ended]');
break;
}
}
Option 3: Node.js with eventsource Package
import EventSource from 'eventsource';
const eventSource = new EventSource(
`https://core.interactor.com/api/v1/agents/rooms/${roomId}/stream`,
{ headers: { 'Authorization': `Bearer ${accessToken}` } }
);
eventSource.addEventListener('token', (e) => {
const data = JSON.parse(e.data);
process.stdout.write(data.token);
});
eventSource.addEventListener('error', (e) => {
console.error('Stream error:', e);
eventSource.close();
});
Reconnection Handling
If the stream disconnects, reconnect with a Last-Event-ID header (if supported):
let lastEventId: string | null = null;
eventSource.onmessage = (e) => {
lastEventId = e.lastEventId;
// ... handle event
};
eventSource.onerror = () => {
eventSource.close();
// Reconnect after delay
setTimeout(() => {
const newSource = new EventSource(
`${streamUrl}?last_event_id=${lastEventId}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
// ... set up handlers
}, 1000);
};
Data Sources
Connect databases and APIs that assistants can query directly using natural language.
Register a Data Source
curl -X POST https://core.interactor.com/api/v1/data-sources \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "sales_database",
"type": "postgresql",
"connection": {
"host": "db.yourcompany.com",
"port": 5432,
"database": "sales",
"username": "readonly_user",
"password": "secure_password"
},
"description": "Sales and customer data including orders, products, and customer information"
}'
Response:
{
"data": {
"id": "ds_abc",
"name": "sales_database",
"type": "postgresql",
"status": "connected",
"schema_status": "extracting",
"created_at": "2026-01-20T12:00:00Z"
}
}
Interactor automatically extracts the database schema for the assistant to understand.
Supported Data Source Types
| Type | Description |
|---|---|
postgresql |
PostgreSQL database |
mysql |
MySQL database |
mssql |
Microsoft SQL Server |
mongodb |
MongoDB database |
rest_api |
REST API endpoint |
List Data Sources
curl https://core.interactor.com/api/v1/data-sources \
-H "Authorization: Bearer <token>"
Get Data Source
curl https://core.interactor.com/api/v1/data-sources/ds_abc \
-H "Authorization: Bearer <token>"
Response includes schema information:
{
"data": {
"id": "ds_abc",
"name": "sales_database",
"type": "postgresql",
"status": "connected",
"schema_status": "ready",
"tables": [
{
"name": "customers",
"columns": [
{"name": "id", "type": "uuid", "nullable": false},
{"name": "email", "type": "varchar", "nullable": false},
{"name": "name", "type": "varchar", "nullable": true},
{"name": "created_at", "type": "timestamp", "nullable": false}
]
},
{
"name": "orders",
"columns": [...]
}
]
}
}
Update Data Source
curl -X PUT https://core.interactor.com/api/v1/data-sources/ds_abc \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"description": "Updated description with more context"
}'
Delete Data Source
curl -X DELETE https://core.interactor.com/api/v1/data-sources/ds_abc \
-H "Authorization: Bearer <token>"
Refresh Schema
Re-extract the schema if your database structure changed:
curl -X POST https://core.interactor.com/api/v1/data-sources/ds_abc/refresh-schema \
-H "Authorization: Bearer <token>"
Execute Query
Run a query directly against the data source:
curl -X POST https://core.interactor.com/api/v1/data-sources/ds_abc/query \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"query": "SELECT * FROM customers WHERE created_at > $1 LIMIT 10",
"parameters": ["2026-01-01"]
}'
Response:
{
"data": {
"columns": ["id", "email", "name", "created_at"],
"rows": [
["uuid-1", "john@example.com", "John Doe", "2026-01-15T10:00:00Z"],
["uuid-2", "jane@example.com", "Jane Smith", "2026-01-16T11:00:00Z"]
],
"row_count": 2,
"execution_time_ms": 45
}
}
Semantic Mappings
Add synonyms and descriptions to help the assistant understand your schema better:
curl -X PATCH https://core.interactor.com/api/v1/data-sources/ds_abc/semantic-mappings \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"mappings": {
"customers": {
"description": "Customer accounts and profiles",
"synonyms": ["users", "clients", "accounts", "members"]
},
"customers.created_at": {
"description": "When the customer signed up",
"synonyms": ["signup date", "registration date", "joined date"]
},
"orders": {
"description": "Customer purchase orders",
"synonyms": ["purchases", "transactions", "sales"]
},
"orders.total_amount": {
"description": "Total order value in USD",
"synonyms": ["order total", "purchase amount", "sale value"]
}
}
}'
This helps the assistant translate natural language questions like:
- "How many users signed up last month?" →
SELECT COUNT(*) FROM customers WHERE created_at >= '2026-01-01' - "What's the total sales this week?" →
SELECT SUM(total_amount) FROM orders WHERE created_at >= '2026-01-14'
Knowledge Base Search
Search for external services that assistants can connect to:
Search Services
curl -X POST https://core.interactor.com/api/v1/knowledge-base/services/search \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"query": "calendar scheduling",
"limit": 5
}'
Response:
{
"data": {
"services": [
{
"id": "google_calendar",
"name": "Google Calendar",
"description": "Calendar and scheduling service",
"auth_type": "oauth2",
"capabilities": ["create_event", "list_events", "update_event", "delete_event"]
},
{
"id": "microsoft_calendar",
"name": "Microsoft Outlook Calendar",
"description": "Microsoft 365 calendar service",
"auth_type": "oauth2",
"capabilities": ["create_event", "list_events", "update_event"]
}
]
}
}
Lookup Service
curl -X POST https://core.interactor.com/api/v1/knowledge-base/services/lookup \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"service_id": "google_calendar"}'
Get Service Details
curl https://core.interactor.com/api/v1/knowledge-base/services/google_calendar \
-H "Authorization: Bearer <token>"
Get Service OAuth Config
curl https://core.interactor.com/api/v1/knowledge-base/services/google_calendar/oauth \
-H "Authorization: Bearer <token>"
Complete Implementation Example
TypeScript Implementation
import { InteractorClient } from './interactor-client';
export class AgentManager {
private client: InteractorClient;
constructor(client: InteractorClient) {
this.client = client;
}
// ============ Assistants ============
async createAssistant(config: {
name: string;
title: string;
description?: string;
instructions: string;
modelConfig?: {
provider?: 'openai';
model?: string;
temperature?: number;
};
enabledTools?: string[];
}): Promise<Assistant> {
return this.client.request('POST', '/agents/assistants', {
name: config.name,
title: config.title,
description: config.description,
instructions: config.instructions,
model_config: config.modelConfig,
enabled_tools: config.enabledTools
});
}
async listAssistants(): Promise<Assistant[]> {
const result = await this.client.request<{ assistants: Assistant[] }>(
'GET',
'/agents/assistants'
);
return result.assistants;
}
async getAssistant(id: string): Promise<Assistant> {
return this.client.request('GET', `/agents/assistants/${id}`);
}
async updateAssistant(id: string
…(truncated)