Skip to main content

Events / Streaming Guide

This guide explains how to use the SDK's Events module to deliver agent execution state to the frontend in real time.

Overview​

The Events module is an asynchronous event delivery system centered around EventEmitter. Events emitted by the agent can be simultaneously delivered to three destinations: SSE (Server-Sent Events), Webhook, and DB.

Agent → EventEmitter → ┬→ SSE Handler     → Frontend
├→ DB Handler → PostgreSQL
└→ Webhook Handler → External System

Event Types​

A list of event types that agents can emit.

About the "UI Display" column

Rendering follows the platform ChatUI. A dedicated UI requires the right metadata — see the implementation guide by display method for what each one needs.

EventTypePurposeUI Display
PHASE_STARTPhase startPhase name
PROGRESS_UPDATEProgress updateProgress message
PROGRESS_MESSAGEProcessing noticeLoading indicator
THOUGHT_MESSAGEAgent thought processThought bubble
TOOL_STARTTool execution startTask card
TOOL_RESULTTool execution completeTask card (updated)
COMPLETION_SUCCESSProcessing complete (success)Completion notification
COMPLETION_FAILUREProcessing complete (failure)Error message
FILE_CREATEDFile generation completeDownload link
HITL_REQUIRED_BROWSER_VNCHuman intervention request for browser operationVNC connection UI
HITL_REQUIRED_BROWSER_CLIHuman intervention request for CLI operationMessage
HITL_COMPLETEDHuman intervention completeCompletion notification
PERMISSION_REQUESTExecution permission requestApproval dialog
PERMISSION_RESPONSEExecution permission responseApproval result
USER_INTERACTION_REQUIREDUser input requestChoice buttons
UNEXPECTED_ERRORUnexpected errorError notification

Implementation Guide by Display Method​

This section groups the implementation needed to actually get an event on screen, by how it displays.

Displayed with message alone (no metadata)​

Just emit with message and it displays. Applies to: PHASE_START / PROGRESS_UPDATE / COMPLETION_SUCCESS / UNEXPECTED_ERROR / PERMISSION_RESPONSE / HITL_REQUIRED_BROWSER_CLI

How each one renders differs per event — see the "UI Display" column above. To show that work is in progress, use PROGRESS_MESSAGE (loading indicator).

Rendered as a thought bubble​

This one needs no metadata either — just emit with message — but it renders in the reasoning display area instead. Applies to: THOUGHT_MESSAGE

Rendered as a task card (tool execution)​

TOOL_START / TOOL_RESULT render as task cards, drawn from the flattened metadata. title is required — it becomes the card's heading.

metadata keyPurpose
titleRequired. The task card heading
descriptionSupplementary text on the card
statusin_progress (clock icon) / completed (check) / error (cross)
actionTypeSelects the dedicated renderer (below). If omitted you still get the title / description fallback
files[]Attached files, shaped {id, filename, fileType, size, url}
metadata.call_idUpdate key for one task. Pass the same value on TOOL_START and TOOL_RESULT to update a single card

Accepted actionType values: command_execution / code_execution / file_operation / browser_action / search_result / image_search / markdown_display

Python
# Tool start — emit one card with the clock icon
await emitter.emit_event(
event_type=EventType.TOOL_START,
message="Running a command",
metadata={
"title": "Command execution",
"status": "in_progress",
"actionType": "command_execution",
"metadata": {"call_id": "call-1", "command": "ls -la"},
},
)

# Tool result — update the card with the same call_id to completed
await emitter.emit_event(
event_type=EventType.TOOL_RESULT,
message="Command finished",
metadata={
"title": "Command execution",
"status": "completed",
"actionType": "command_execution",
"metadata": {"call_id": "call-1", "command": "ls -la", "output": "..."},
},
)

Rendered as files​

Use metadata.files[] for in-progress files and metadata.deliverable_files[] for deliverables in the completion body (file_name / download_url are not used). Each entry in files[] is shaped {id, filename, fileType, size, url}. See the storage guide for the exact shape. Applies to: FILE_CREATED / COMPLETION_SUCCESS

Rendered as choice / confirmation buttons​

Add metadata.interaction_type and a button UI appears under the message.

  • "choice" + options[] → choice buttons; pressing one submits that value as-is. options accepts at most 10 entries, strings only, each at most 200 characters. Applies to: USER_INTERACTION_REQUIRED
  • "confirmation" (no options) → yes / no buttons. Applies to: PERMISSION_REQUEST
Python
# Offer choices and let the user pick
await emitter.emit_event(
event_type=EventType.USER_INTERACTION_REQUIRED,
message="Which output format do you want?",
metadata={"interaction_type": "choice", "options": ["PDF", "Excel", "Markdown"]},
)

# Ask for approval with yes / no
await emitter.emit_event(
event_type=EventType.PERMISSION_REQUEST,
message="May I delete this file?",
metadata={"interaction_type": "confirmation"},
)

Rendered as a loading indicator​

Emit PROGRESS_MESSAGE to show the animated loading indicator.

Python
await emitter.emit_event(
event_type=EventType.PROGRESS_MESSAGE,
message="Searching internal documents...",
)

Rendered as the VNC connection UI​

For HITL_REQUIRED_BROWSER_VNC / HITL_COMPLETED, put the following keys into metadata as-is.

metadata keyPurpose
type"vnc_popup" (intervention required) / "vnc_completed" (done)
execution_idThe execution this refers to
vnc_urlVNC URL to connect to
cancel_urlURL that cancels the intervention
vnc_portVNC port
intervention_typeKind of intervention
instructionsWhat the user should do
reasonWhy the intervention became necessary
Python
await emitter.emit_event(
event_type=EventType.HITL_REQUIRED_BROWSER_VNC,
message="Please complete the login",
metadata={
"type": "vnc_popup",
"execution_id": os.environ["EXECUTION_ID"],
"vnc_url": vnc_url,
"cancel_url": cancel_url,
"vnc_port": 5901,
"intervention_type": "browser_login",
"instructions": "Log in on the screen that opens, then close it.",
"reason": "Reached a page that requires authentication",
},
)

Closing the turn​

Always put the text you want displayed into the completion event's message. Close the turn — on success and on failure alike — by emitting COMPLETION_SUCCESS (finish_reason=stop) as the terminal event. When reporting a failure, include the failure details in message / metadata (the execution's own outcome is recorded separately via PodRuntime.final(status=...)).

COMPLETION_FAILURE does not close the turn. Put the final response in the COMPLETION_SUCCESS body.

Python
try:
await run_agent_logic()
except Exception as e:
# Close the turn with COMPLETION_SUCCESS (finish_reason=stop) even on failure.
await emitter.emit_event(
event_type=EventType.COMPLETION_SUCCESS,
message=f"An error occurred: {e}",
metadata={"error_type": type(e).__name__, "status": "failed"},
)
finally:
# Always execute cleanup
await emitter.cleanup()

Basic Usage​

Initializing and Using EventEmitter​

emit_event() accepts message (string) and metadata (dictionary, optional).

Python
from agenticstar_platform.events import EventEmitter, EventType, SubEventType

# Initialize EventEmitter (execution_id is required)
emitter = EventEmitter(execution_id="exec-abc-123")

# Phase start event
await emitter.emit_event(
event_type=EventType.PHASE_START,
message="Analyzing intent...",
metadata={"phase": "intent"},
)

# Streaming thought process
await emitter.emit_event(
event_type=EventType.THOUGHT_MESSAGE,
message="The user is requesting a RAG search",
sub_event_type=SubEventType.SEARCH_WEB,
)

# Tool execution
await emitter.emit_event(
event_type=EventType.TOOL_START,
message="Executing search_knowledge",
metadata={"tool_name": "search_knowledge", "input": {"query": "AI agent"}},
)

# Tool result
await emitter.emit_event(
event_type=EventType.TOOL_RESULT,
message="Search complete: 5 results",
metadata={"tool_name": "search_knowledge", "result_count": 5},
)

# Completion
await emitter.emit_event(
event_type=EventType.COMPLETION_SUCCESS,
message="Response complete",
)

# Cleanup (required)
await emitter.cleanup()

For the terminal-event contract, see closing the turn.

Automatic Sequence Number Management​

EventEmitter automatically assigns sequence numbers to emitted events. The frontend can use these numbers to guarantee event ordering.

Python
# Sequence number is available as a property
print(emitter.sequence_number) # 0, 1, 2, ... auto-incremented

Event Handlers​

EventEmitter accepts a single handler in its constructor.

SSE Handler (Frontend Delivery)​

Python
from agenticstar_platform.events import EventEmitter, create_sse_handler

# Combine with FastAPI's StreamingResponse
async def chat_stream(request: ChatRequest):
sse_handler = create_sse_handler()
emitter = EventEmitter(execution_id="exec-123", handler=sse_handler)

# Start agent processing asynchronously
asyncio.create_task(run_agent(emitter, request))

# Return SSE stream
return StreamingResponse(
emitter.consume_events(),
media_type="text/event-stream",
)

DB Handler (Persistence)​

Python
from agenticstar_platform.events.handlers import DatabaseEventHandler

# Persist events to PostgreSQL with DB handler
db_handler = DatabaseEventHandler(
data_access=data_access,
user_id="user-001",
conversation_id="conv-001",
message_id="msg-001",
)
emitter = EventEmitter(execution_id="exec-123", handler=db_handler)

Webhook Handler (External Notification)​

Python
from agenticstar_platform.events.handlers import WebhookEventHandler

# Send webhook notifications to external systems
webhook_handler = WebhookEventHandler(
webhook_url="https://your-system.example.com/webhook",
conversation_id="conv-001",
message_id="msg-001",
)
emitter = EventEmitter(execution_id="exec-123", handler=webhook_handler)

Composite Handler​

Multiple handlers can be combined and managed as one.

Python
from agenticstar_platform.events.handlers import CompositeEventHandler

composite = CompositeEventHandler([
create_sse_handler(),
DatabaseEventHandler(data_access=da, user_id="u", conversation_id="c", message_id="m"),
WebhookEventHandler(webhook_url="https://...", conversation_id="c", message_id="m"),
])
emitter = EventEmitter(execution_id="exec-123", handler=composite)

SubEventType and actionType​

sub_event_type alone does not change the rendering. To vary how a tool execution renders, set metadata.actionType using the table below. sub_event_type remains useful as a classification in the DB and in logs.

SubEventType → actionType mapping​

SubEventTypemetadata.actionType
SEARCH_WEBsearch_result
COMMAND_EXECUTION / BASH_EXECUTEDcommand_execution
FILE_OPERATION / FILE_EDITED / FILE_READfile_operation
IMAGE_GENERATEDimage_search (image preview family)
Anything elseLeave it unset (title / description fallback) or pick the closest value
Python
# metadata.actionType is what varies the rendering; sub_event_type is for classification
await emitter.emit_event(
event_type=EventType.TOOL_START,
message="Executing web search...",
sub_event_type=SubEventType.SEARCH_WEB,
metadata={
"title": "Web search",
"status": "in_progress",
"actionType": "search_result",
"metadata": {"call_id": "call-2", "query": "AI agent"},
},
)

SubEventType reference​

SubEventTypePurpose
SEARCH_WEBWeb search in progress
COMMAND_EXECUTIONCommand execution in progress
FILE_OPERATIONFile operation in progress
FILE_EDITEDFile edit complete
FILE_READFile read complete
FILE_SEARCHEDFile search complete
BASH_EXECUTEDBash command execution complete
WEB_FETCHEDWeb page fetch complete
MCP_TOOLMCP tool execution in progress
LOCAL_ASSISTANTLocal assistant processing
TASK_LAUNCHEDTask launched
TODO_UPDATEDTODO updated
VIDEO_GENERATEDVideo generation complete
IMAGE_GENERATEDImage generation complete
SLIDE_CREATEDSlide creation complete
MACOS_AUTOMATIONmacOS automation in progress

Marketplace Handler​

For agents provided through the marketplace, use the dedicated handler factory. It generates a composite handler for DB + Webhook in one step.

Python
import os

from agenticstar_platform.events import EventEmitter
from agenticstar_platform.events.handlers import create_marketplace_handler

# Read runtime identifiers from the environment variables injected by the platform
handler = create_marketplace_handler(
data_access=data_access,
webhook_url="https://tenant.example.com/webhook",
user_id=os.environ["USER_ID"],
conversation_id=os.environ["CONVERSATION_ID"],
message_id=os.environ["MESSAGE_ID"],
request_source="external", # Set when delivering responses via the external API
)
emitter = EventEmitter(execution_id=os.environ["EXECUTION_ID"], handler=handler)
SDK 0.5.29+

The whole execution lifecycle — this handler wiring, identity handoff, the terminal event (exactly once), and cleanup — is handled in one call by run_marketplace_agent(my_agent) (see the runner section in the quickstart). Using create_marketplace_handler directly is the low-level API for when you want to compose the handler yourself.

Error Handling​

Python
try:
await run_agent_logic()
except Exception as e:
# Emit error event to notify the frontend
await emitter.emit_event(
event_type=EventType.COMPLETION_FAILURE,
message=str(e),
metadata={"error_type": type(e).__name__},
)
finally:
# Always execute cleanup
await emitter.cleanup()

Next Steps​

SDK API Reference — Events Module

Complete specifications for EventEmitter / StreamingEvent / EventType

View guide

Architecture Guide

SDK module structure and design philosophy

View guide