Agent Execution
Execute agents via Server-Sent Events (SSE) streaming and manage conversation sessions.
Authentication
All execution endpoints support hybrid authentication:
| Method | Header |
|---|---|
| JWT | Authorization: Bearer <jwt> + x-tenant-id |
| API Key | X-Api-Key: YOUR_API_KEY |
POST /api/v2/agents/:agent_id/stream
Execute an agent with real-time SSE streaming.
Path Parameters:
| Param | Type | Description |
|---|---|---|
agent_id | UUID | Agent ID |
Request Body:
{
"message": "List active contracts",
"session_id": "uuid (optional)",
"attachments": [
{
"url": "https://example.com/image.png",
"type": "image"
}
],
"consumer_key": "string (optional, admin/owner API key)",
"context": { "cliente": "Ana" },
"bindings": { "idSolicitante": "u-4821" },
"session_from": ["idSolicitante"],
"options": {
"timeout_ms": 60000
},
"metadata": {
"trace_id": "string",
"correlation_id": "string"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | User message |
session_id | UUID | No | Session ID to maintain conversation context |
attachments | array | No | Multimodal files (see Media) |
consumer_key | string | No | Person identity. Admin/owner API key only |
context | object | No | Data the agent reads. Admin/owner API key only |
bindings | object | No | Authoritative tool arguments, never shown to the model. Admin/owner API key only |
session_from | string[] | No | Derives the session from context/bindings fields. Do not combine with session_id |
options | object | No | Only options.timeout_ms has effect (shortens the turn deadline). Other keys, such as temperature and max_tokens, are ignored and reported in ignored_fields — agent settings live in llm_config |
client_message_id | UUID | No | Id of this message generated by your client. Resending the same id after a finished turn replays the answer without running the agent again (24 h). See the contract |
metadata | object | No | Correlation only (trace_id…). Stored on the execution, never sent to the model or tools; it does not change agent behavior |
Full rules, limits and examples: Agent invocation contract.
Attachments
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | File URL or base64 data URI |
type | string | No | image, audio, document (auto-detect if omitted) |
filename | string | No | Filename (helps type detection) |
Supported types:
- image: jpg, jpeg, png, gif, webp, bmp
- audio: mp3, wav, ogg, flac, m4a, webm, mp4
- document: pdf, xlsx, xls, docx, csv, txt
SSE Response
See Streaming for the complete event format.
The metadata event carries the turn's execution_id; the response and done events carry execution_id, user_message_id and assistant_message_id (the same ids the session messages pagination returns; null when nothing was recorded). A second message in the same conversation while the first is being answered gets error with code: "SESSION_BUSY". See Agent invocation contract.
Errors:
| Code | Status | Description |
|---|---|---|
VALIDATION_ERROR | 400 | Missing message field |
AGENT_NOT_FOUND | 403 | Agent not available |
AGENT_ARCHIVED | — | Archived agent — execution blocked |
SESSION_BUSY | 409 | Another turn is still being answered in this conversation (or the same client_message_id is in flight) |
IDEMPOTENCY_CONFLICT | 409 | The client_message_id was already used with a different message |
STREAM_ERROR | 500 | Error during streaming |
GET /api/v2/agents
List the agents available to the caller. Each item includes id, name, commercial_name, bio, type, status, tags, tools_count, system_label and avatar_url (the last two are null when not set). avatar_url and system_label are returned for every role, including admin, editor and owner.
GET /api/v2/agents/:agent_id/sessions
List conversation sessions for an agent.
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Max sessions returned |
offset | integer | 0 | Pagination offset |
include_expired | boolean | false | Include expired sessions |
Response:
{
"success": true,
"data": [
{
"id": "ee59c2b3-1466-4367-a365-7d62a9d93a2d",
"created_at": "2026-01-14T10:30:00.000Z",
"updated_at": "2026-01-14T11:45:00.000Z",
"message_count": 8,
"last_message_preview": "Found 5 active contracts for the client...",
"last_message_role": "assistant"
}
],
"count": 2,
"agent_id": "uuid"
}
Session Management
Sessions are managed through the /api/v1/sessions endpoints.
Session Expiration (TTL by Plan)
| Plan | Session TTL |
|---|---|
free | 24 hours |
basic | 7 days |
pro | 7 days |
enterprise | 30 days |
- Each user interaction renews
expires_atbased on the current plan - Expired sessions are deleted by a cleanup job (every 6 hours)
Key Endpoints
| Endpoint | Method | Description |
|---|---|---|
GET /api/v1/sessions | GET | List sessions with filters: status, agent_id, start_date, end_date, control_mode (engaged|suspended|manual_handoff), consumer_key, page, limit, order_by, order_dir. An unknown status returns 400 listing the accepted values; meta.has_more tells whether there is a next page |
GET /api/v1/sessions/active | GET | Currently executing sessions |
GET /api/v1/sessions/stats | GET | Aggregated metrics |
GET /api/v1/sessions/:sessionId | GET | Session details with conversation history, system prompt, control_mode, control_metadata, and active_turn (see Turn in progress) |
GET /api/v1/sessions/:sessionId/tool-calls | GET | Granular tool call logs (input, output, duration) |
GET /api/v1/sessions/:executionId/live | GET | Real-time SSE stream |
POST /api/v1/sessions/:sessionId/archive | POST | Archive session |
POST /api/v1/sessions/:sessionId/clear | POST | Clear conversation history |
POST /api/v1/sessions/:sessionId/compact | POST | Compact session (replace old messages with LLM summary) |
POST /api/v1/sessions/:sessionId/suspend | POST | Suspend the agent for this session (control_mode='suspended'). Optional body: {reason?, message?}. Requires sessions:update. |
POST /api/v1/sessions/:sessionId/resume | POST | Resume the agent (control_mode='engaged'). |
POST /api/v1/sessions/:sessionId/handoff | POST | Mark the session as taken by a human operator (control_mode='manual_handoff'). |
POST /api/v1/sessions/:sessionId/cancel | POST | Cancel the in-progress turn (mid-LLM, mid-tool). Optional body: {reason?, execution_id?}. Returns {cancelled, local_aborted, published, reason, session_id, scope, execution_id?}. |
POST /api/v1/sessions/:sessionId/manual-message | POST | Send a manual operator message through the agent's outbound channel. Requires control_mode != engaged. Body: {message: string}. Header Idempotency-Key optional (5 min cache for safe retries). |
POST /api/v1/sessions/batch/archive | POST | Archive multiple sessions (max 50) |
POST /api/v1/sessions/batch/compact | POST | Batch compact eligible sessions |
Session Control Modes
Sessions have a control_mode field that controls whether the agent processes new messages:
| Mode | Behavior |
|---|---|
engaged (default) | Agent processes messages normally |
suspended | Agent loop is bypassed for new messages. Use to pause temporarily (review, investigation). Optional control_metadata.expires_at triggers automatic resume. |
manual_handoff | Same bypass as suspended, but signals that a human operator is handling the conversation. Use POST /:sessionId/manual-message to send messages through the agent's outbound channel. |
When a message arrives for a non-engaged session, the platform records the control change for operator dashboards and returns an empty acknowledgment to the channel (no LLM call, no quota consumed).
Cancel vs Suspend
/suspendblocks the next turn on this session. The current turn (if in progress) completes normally./cancelaborts the current turn in flight (mid-LLM, mid-tool, mid-stream). The next message is processed normally unless the session is also suspended.
Cancelling a turn
- The user who owns the conversation (including the
memberrole) can cancel their own turn. An operator can cancel any session of the company. A session from another company returns404. - Body:
{ "reason": "…", "execution_id": "…" }, both optional. Withexecution_id(the one in the stream'smetadataevent), only that turn is cancelled, and it also works if the request arrives before the turn has started. Without it, the turn in progress in the session is cancelled. - The response keeps
cancelled,local_aborted,publishedandreason, and addssession_id,scope(sessionorexecution) andexecution_id(when sent). - Closing the
/streamconnection still cancels the turn. For an explicit "Stop" button, prefer calling/cancelwith theexecution_id.
Session Compaction
Compaction replaces old messages with an LLM-generated summary, preserving recent messages intact.
Compaction thresholds scale with your plan — higher plans keep longer histories and preserve more recent messages before a summary kicks in.
Derived Sessions (Webhooks)
Sessions created via webhooks with session_strategy.mode: 'derive' use deterministic session IDs derived from the sender's identity: the same user always generates the same session_id.
When an archived derived session receives a new message:
- The same deterministic
session_idis generated - No active session is found
- A new session is created with clean history
- The session is reactivated automatically
Turn in progress
GET /api/v1/sessions/:sessionId returns data.session.active_turn: { "execution_id": "...", "started_at": "..." } while a turn is running in that conversation, or null otherwise (and when it could not be determined). It only reflects conversations driven through POST /api/v2/agents/:agent_id/stream. Use the execution_id to cancel that specific turn.
Interrupted replies
When the user stops a reply, or the turn hits its time limit, the agent message is kept in the history with metadata.interrupted set to user or timeout. The partial text is stored only when it is safe to do so; otherwise the message is a short platform note saying the reply was interrupted. The agent is aware that its previous reply was cut.
GET /api/v1/me/events
Server-Sent Events stream with the signed-in user's conversation list changes (user token only). Events:
| Event | When | Main fields |
|---|---|---|
session_updated | A turn ended and the conversation changed | session_id, agent_id, last_message_role, last_message_preview, execution_id (the turn that caused it), at |
turn_started | A turn started in one of the user's conversations | session_id, agent_id, execution_id, started_at, at |
turn_finished | The turn ended (including with an error) | session_id, agent_id, execution_id, outcome, at |
turn_started and turn_finished are emitted only for turns started through /stream. A client that sent the turn itself can match execution_id to ignore its own session_updated.
GET /api/v1/me/unread-by-tenant
Unread conversations and pending approvals for each company the signed-in user is an active member of. Requires a user token (JWT); an API key returns 403.
{
"success": true,
"data": [
{ "tenant_id": "uuid", "tenant_name": "Acme", "unread": 3, "pending_approvals": 1 }
],
"meta": { "total": 1 }
}
| Field | Description |
|---|---|
unread | Conversations with unread agent messages |
pending_approvals | Approvals awaiting this user's decision |
GET /api/v1/sessions/:sessionId/tool-calls
Get detailed tool call logs for a session. Each entry includes the tool's input arguments, output result, execution duration, and success/failure status.
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
iteration | integer | — | Filter by agent loop iteration |
tool_name | string | — | Filter by tool name |
Response:
{
"success": true,
"data": [
{
"id": "uuid",
"tool_name": "search_contracts",
"tool_call_id": "call_abc123",
"tool_input": { "status": "active", "limit": 10 },
"tool_output": { "rows": [...], "count": 5 },
"output_preview": "5 rows returned",
"success": true,
"duration_ms": 450,
"error_message": null,
"iteration": 1,
"call_index": 0,
"execution_id": "uuid",
"created_at": "2026-02-19T14:30:01Z"
}
],
"count": 1,
"session_id": "a1b2c3d4-..."
}
Tool call logs are recorded starting from a previous release. Sessions from before this update will return an empty array.
Conversation History Normalization
Starting from a previous release, the agent loop persists the complete tool call sequence internally (including role: 'tool' messages and assistant messages with tool_calls). However, all session endpoints normalize the conversation_history before returning:
- Messages with
role: 'tool'are filtered out (internal to the agent loop) - Assistant messages that only contain
tool_calls(no text content) are filtered out - Technical fields (
tool_calls,tool_call_id,name) are stripped from remaining messages - The
message_countin session lists reflects only renderable messages (user + assistant with content)
This ensures API consumers always receive the familiar {role: 'user'|'assistant', content: string} format.
Template Variables in System Prompt
The agent's system prompt supports dynamic template variables that are resolved at execution time. This allows personas to reference current date/time without manual updates.
| Variable | Description | Example |
|---|---|---|
{{current_date}} | Today's date (pt-BR format) | 02/03/2026 |
{{current_time}} | Current time (pt-BR format) | 14:30:00 |
{{current_weekday}} | Weekday name (pt-BR) | segunda-feira |
{{agent_name}} | The agent's name | Sales Assistant |
Variables are interpolated after the system prompt is assembled and before it is sent to the LLM. Unrecognized variables (e.g., {{custom_field}}) are preserved as-is.
System Prompt Snapshot
The GET /api/v1/sessions/:sessionId response includes a top-level system_prompt field containing the exact system prompt used in the first turn of the session. This is captured once and never overwritten, even if the agent's persona changes later.
{
"data": {
"session": { "..." },
"conversation_history": [ "..." ],
"metrics": { "..." },
"logs": [ "..." ],
"system_prompt": "# Agent Name\n\n## Identidade\n..."
}
}
| Scenario | system_prompt value |
|---|---|
| Session created after this feature was introduced | Full system prompt string |
| Session created in older versions | null |
| Session with no interactions yet | null |
GET /api/v2/agents/:agent_id/tools
Get the complete JSON Schema for each tool available to the agent.
Response:
{
"success": true,
"data": [
{
"name": "list_contracts",
"description": "List active client contracts",
"parameters": {
"type": "object",
"properties": {
"customer_id": {
"type": "string",
"description": "Client ID"
}
},
"required": ["customer_id"]
}
}
],
"count": 39,
"agent_id": "uuid"
}
Examples
Execute Agent (Stream)
- curl
- Node.js
- Python
curl -N "https://llm.zihin.ai/api/v2/agents/AGENT_ID/stream" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"message": "List active contracts"}'
const response = await fetch(
'https://llm.zihin.ai/api/v2/agents/AGENT_ID/stream',
{
method: 'POST',
headers: {
'X-Api-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: 'List active contracts' }),
}
);
// Read SSE stream
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
process.stdout.write(decoder.decode(value));
}
import requests
response = requests.post(
"https://llm.zihin.ai/api/v2/agents/AGENT_ID/stream",
headers={
"X-Api-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
json={"message": "List active contracts"},
stream=True,
)
# Read SSE stream
for chunk in response.iter_content(chunk_size=None):
print(chunk.decode(), end="")
Execute with Attachment
curl -N -X POST "https://llm.zihin.ai/api/v2/agents/AGENT_ID/stream" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "What do you see in this image?",
"attachments": [
{ "url": "https://example.com/photo.jpg", "type": "image" }
]
}'
Continue Conversation
curl -N "https://llm.zihin.ai/api/v2/agents/AGENT_ID/stream" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type": "application/json" \
-d '{"message": "Show more details", "session_id": "SESSION_ID"}'