SSE Events
Server-Sent Events for real-time updates to agents and dashboards. Two separate streams with different event types and payloads.
Right page if: you are debugging SSE connections, building a custom SSE client, or need to know which events fire on which actions (agent stream vs dashboard stream). Wrong page if: you need the HTTP API endpoints -- see https://docs.edictum.ai/docs/console/reference/api. For connecting agents to the console, see https://docs.edictum.ai/docs/console/connecting-agents. Gotcha: there are two separate SSE streams (agent at /api/v1/stream, dashboard at /api/v1/stream/dashboard) with different event types and auth. The agent stream filters `contract_update` events by `bundle_name` if provided on connection.
Edictum Console uses Server-Sent Events (SSE) to push real-time updates to agents and dashboards. Two separate streams serve different audiences with different event types.
Agent Stream
GET /api/v1/stream?env={env}&bundle_name={name}&policy_version={hash}&tags={json}
Auth: API key (Authorization: Bearer edk_...)Agents connect to this endpoint to receive contract updates, approval decisions, and assignment changes in real time.
Query Parameters
| Parameter | Required | Description |
|---|---|---|
env | Yes | Environment to subscribe to (e.g., production, staging) |
bundle_name | No | Filter contract_update events to this bundle only. When omitted, all contract_update events for the tenant are forwarded. |
policy_version | No | SHA-256 revision hash the agent is currently running. Used for drift detection on the fleet status endpoint. |
tags | No | JSON-encoded agent tags (e.g., {"team": "platform"}). Used for assignment rule matching. |
Authentication Error
Connecting without an Authorization header, or with an invalid or revoked API key, returns 401 Unauthorized. The connection is not established. This replaces the previous 422 behavior — any tooling that previously checked for 422 on auth failure should be updated to check for 401.
Environment Scope Enforcement
The env query parameter must match the API key's scoped environment. If they differ, the server rejects the connection immediately:
HTTP/1.1 403 Forbidden
{"detail": "API key is scoped to 'production', cannot subscribe to 'staging'."}This check runs before any SSE headers are sent. Agents using a key for production cannot subscribe to staging events, and vice versa.
Connection Behavior
On connect, the stream endpoint:
- Validates the API key and resolves
tenant_id+env - Auto-registers the agent if not already known (creates an
agent_registrationsrow) - Resolves bundle assignment if
bundle_nameis not provided (explicit assignment -> rule match -> agent-provided). Rule matching is scoped to the agent's authenticated environment — agents in different environments receive independent, environment-filtered contract bundles. - Subscribes the connection to
PushManager._connections[env] - Sends a keepalive comment (
: keepalive) periodically
On disconnect, the connection is removed from the PushManager. Stale connections are cleaned up every 5 minutes.
Agent Events
contract_update
Fired when a bundle is deployed to the agent's environment.
event: contract_update
data: {
"type": "contract_update",
"bundle_name": "devops-agent",
"version": 3,
"revision_hash": "abc123def456...",
"signature": "ed25519-hex-signature|null",
"public_key": "ed25519-hex-public-key|null",
"yaml_bytes": "base64-encoded-yaml-content"
}| Field | Type | Description |
|---|---|---|
type | string | Always "contract_update" |
bundle_name | string | Name of the deployed bundle |
version | int | Bundle version number |
revision_hash | string | SHA-256 hash of the YAML content |
signature | string|null | Hex-encoded Ed25519 signature |
public_key | string|null | Hex-encoded Ed25519 public key for verification |
yaml_bytes | string | Base64-encoded YAML bundle content |
The SDK decodes yaml_bytes from base64 and passes it to Edictum.reload() for atomic contract swap. If bundle_name was provided on connection, only updates matching that bundle are forwarded.
approval_decided
Fired when a human approves or denies a pending approval request for this agent.
event: approval_decided
data: {
"type": "approval_decided",
"approval_id": "uuid",
"status": "approved",
"decided_by": "admin@example.com"
}| Field | Type | Description |
|---|---|---|
type | string | Always "approval_decided" |
approval_id | string | UUID of the approval request |
status | string | "approved" or "denied" |
decided_by | string | Email or identifier of the decision-maker |
Note: decision_reason is not included in the SSE payload. To get the full decision details, fetch GET /api/v1/approvals/{approval_id}.
assignment_changed
Fired when an agent's bundle assignment changes (via bulk assign or rule update).
event: assignment_changed
data: {
"type": "assignment_changed",
"agent_id": "my-agent",
"bundle_name": "new-bundle",
"source": "explicit"
}| Field | Type | Description |
|---|---|---|
type | string | Always "assignment_changed" |
agent_id | string | The affected agent |
bundle_name | string|null | New bundle assignment (null if unassigned) |
source | string | Assignment source: "explicit", "rule", or "none" |
Dashboard Stream
GET /api/v1/stream/dashboard
Auth: Session cookie (HttpOnly)Dashboard users connect to this endpoint to receive real-time updates across all environments for their tenant.
Dashboard Events
The dashboard stream forwards events matching this whitelist:
| Event Type | Trigger |
|---|---|
approval_created | Agent creates a new approval request |
approval_decided | Human approves or denies an approval |
approval_timeout | Pending approval expires |
assignment_changed | Agent bundle assignment modified |
bundle_uploaded | New bundle version uploaded |
composition_changed | Composition created, updated, or deleted |
contract_created | New contract added to library |
contract_update | Bundle deployed to an environment |
contract_updated | Existing contract version updated |
event_created | New audit event ingested |
api_key_created | New API key created |
api_key_revoked | API key revoked |
bundle_deployed | Bundle deployed to environment |
signing_key_rotated | Ed25519 signing key rotated |
approval_created
{
"type": "approval_created",
"approval_id": "uuid",
"agent_id": "my-agent",
"tool_name": "delete_user",
"message": "Agent wants to delete user account"
}Note: env, timeout_seconds, and contract_name are not included in the SSE payload. The SSE event provides minimal identifying info; fetch GET /api/v1/approvals/{approval_id} for full details.
approval_timeout
{
"type": "approval_timeout",
"approval_id": "uuid",
"agent_id": "my-agent",
"tool_name": "delete_user"
}bundle_uploaded
{
"type": "bundle_uploaded",
"bundle_name": "devops-agent",
"version": 3,
"revision_hash": "abc123...",
"uploaded_by": "user_123"
}event_created
{
"type": "event_created",
"accepted": 3
}Note: This is a per-batch summary, not per-event. The accepted field is the number of events ingested in the batch. Pushed to dashboard only (not to agents).
AI Assistant Stream
POST /api/ai/assist
Auth: Session cookie (HttpOnly)The AI assistant endpoint streams responses as SSE. In addition to content_chunk events carrying streamed text, two events are emitted during agentic turns when the assistant invokes tools.
AI Assistant Events
tool_call_start
Emitted when the LLM invokes a tool.
{
"type": "tool_call_start",
"tool": "validate_contract",
"id": "toolu_01Abc123"
}| Field | Type | Description |
|---|---|---|
type | string | Always "tool_call_start" |
tool | string | Name of the tool being called (e.g., "validate_contract", "evaluate_contract") |
id | string | Unique identifier for this tool invocation; matches the corresponding tool_call_result |
tool_call_result
Emitted when a tool invocation completes.
{
"type": "tool_call_result",
"id": "toolu_01Abc123",
"tool": "validate_contract",
"result": { "valid": true, "errors": [] },
"duration_ms": 42
}| Field | Type | Description |
|---|---|---|
type | string | Always "tool_call_result" |
id | string | Matches the id from the corresponding tool_call_start |
tool | string | Name of the tool that was called |
result | object | Tool execution output (schema varies by tool) |
duration_ms | int | Execution time in milliseconds |
These two events appear between content_chunk events during agentic turns. The UI uses them to render collapsible tool call indicators with status icons and duration.
Ollama does not emit these events. Ollama-backed responses are text-only; no tool calls are made and no tool_call_start or tool_call_result events are emitted.
PushManager Architecture
The PushManager is an in-process event dispatcher using asyncio queues. No external message broker is required.
PushManager
+-- _connections: dict[env, list[AgentConnection]]
| Each AgentConnection holds:
| +- queue: asyncio.Queue
| +- env, tenant_id, agent_id
| +- bundle_name, policy_version
| +- connected_at, is_closed
|
+-- _dashboard_connections: dict[tenant_id, list[DashboardConnection]]
| Each DashboardConnection holds:
| +- queue: asyncio.Queue
| +- tenant_id, connected_atDispatch Methods
| Method | Target | Filtering |
|---|---|---|
push_to_env(env, data, tenant_id) | All agents in environment | Tenant match. For contract_update events, additionally filters by bundle_name -- only agents subscribed to that bundle receive the event. |
push_to_dashboard(tenant_id, data) | All dashboard connections for tenant | Event type must be in the whitelist. |
push_to_agent(agent_id, data, tenant_id) | Specific agent across all environments | Exact agent_id + tenant_id match. |
Connection Lifecycle
- Subscribe: Agent/dashboard connects, a new
asyncio.Queueis created and added to the connection map - Receive: The SSE endpoint reads from the queue in a loop, yielding events as SSE text
- Unsubscribe: On disconnect, the connection is removed from the map
- Cleanup: A background task runs every 5 minutes, removing connections that are closed or older than 1 hour
Event Format on the Wire
Events are sent as standard SSE with the event type derived from data["type"]:
event: contract_update
data: {"type": "contract_update", "bundle_name": "devops-agent", ...}
event: approval_created
data: {"type": "approval_created", "approval_id": "...", ...}The event: line enables EventSource clients to listen for specific event types with addEventListener().
Last updated on