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