Provider Command And Event Flow Guide
This guide explains how provider-side command intents, next actions, and history events move through the codebase and reach an OpAMP client.
It is intended to serve two audiences:
- developers adding or refactoring a provider-side command or client-directed action
- tool-assisted workflows, including prompts for tools such as Codex
Scope
This guide covers:
- provider REST/UI command and client-action entry points
- in-memory state and history behavior
- response building and transport send behavior
- how standard commands, custom commands, remote config, identification, heartbeat changes, and other next actions differ
- developer expectations around history creation
This guide does not attempt to document consumer-side custom handler internals in detail. For that, see:
Key Rule
Treat provider UI/API activity and MCP activity differently.
- Provider UI/API flows define the user-visible client history behavior.
- MCP activity should be excluded from the history rules described here.
- If an MCP path currently reuses a history-producing helper, treat that as an implementation detail or technical debt, not the desired contract for future work.
In other words: do not use MCP behavior as the reference model when deciding whether a provider-side client action should create a history entry.
High-Level Model
There are three related but distinct concepts in the provider:
CommandRecordUsed for queued command intents such as restart, force resync, and custom commands. These live inClientRecord.commands.next_actionsUsed for provider-directed actions that are not stored asCommandRecordobjects, such as applying remote config or changing connection settings.EventHistoryUsed for user-visible history items that are not naturally represented as commands, or when the route adds history manually.
The key design consequence is that not every client-directed operation follows the same storage or history path.
Code Map
Primary files:
provider/src/opamp_provider/app_routes_clients.pyprovider/src/opamp_provider/command_queue.pyprovider/src/opamp_provider/state.pyprovider/src/opamp_provider/app_response_builder.pyprovider/src/opamp_provider/app.pyprovider/src/opamp_provider/commands.py
Related docs:
- Command Process Implementation Note
- Adding Your Own Custom Action
- Provider Server Diagram (Mermaid Source)
Architecture Overview
flowchart LR
UI["Provider UI / REST client"] --> ROUTES["app_routes_clients.py"]
MCP["MCP tools"] -. excluded from history rules .-> CQ
ROUTES --> CQ["command_queue.py"]
ROUTES --> STORE["ClientStore in state.py"]
CQ --> STORE
STORE --> RESP["ServerToAgentResponseBuilder"]
RESP --> HTTP["OpAMP HTTP response"]
RESP --> WS["OpAMP WebSocket response"]
HTTP --> AGENT["Agent"]
WS --> AGENT
STORE --> HIST["ClientRecord.events"]
STORE --> CMDS["ClientRecord.commands"]
STORE --> NEXT["ClientRecord.next_actions"]
Command And Event Surfaces
The provider currently uses several different surfaces to send data to a client.
| Surface | Route | State primitive | History behavior | Sent over |
|---|---|---|---|---|
| Standard/custom command | POST /api/clients/<client_id>/commands |
CommandRecord in commands |
Automatic via queue_command(...) |
HTTP and WebSocket |
| Heartbeat change | PUT /api/clients/<client_id>/heartbeat-frequency |
client field + event | Automatic inside set_client_heartbeat_frequency(...) |
no dedicated send token in the current route; stored state is available to connection-settings builders |
| Issue new unique ID | POST /api/clients/<client_id>/identify |
pending identification field | Manual add_event(...) in route |
HTTP and WebSocket |
| Requested config text | POST /api/clients/<client_id>/config |
requested_config fields |
No history in current route | separate effective config flow, not a queued command |
| Remote config files | POST /api/clients/<client_id>/remote-config |
pending remote config + next_actions |
Manual add_event(...) in route |
HTTP next action (apply_config) |
| Generic next actions | POST /api/clients/<client_id>/actions |
next_actions |
No history in current route | HTTP only |
This table is the core reason developers need to be explicit: only some paths create history as part of the state mutation itself.
The Preferred Pattern For True Commands
For provider UI/API initiated commands, the cleanest pattern is:
- validate and normalize request payload
- create a
CommandRecord - append it to
ClientRecord.commands - append the same intent to unified event history
- build the outbound OpAMP payload on the next client check-in
- mark the command sent once a command payload was actually emitted
That pattern is implemented by:
queue_command_from_payload(...)incommand_queue.pystore.queue_command(...)instate.py
Standard/Custom Command Flow
sequenceDiagram
participant UI as Provider UI/API
participant Routes as app_routes_clients.py
participant Queue as command_queue.py
participant Store as state.py
participant Builder as app_response_builder.py
participant Agent as OpAMP Agent
UI->>Routes: POST /api/clients/<id>/commands
Routes->>Queue: queue_command_from_payload(...)
Queue->>Store: queue_command(...)
Note over Store: append CommandRecord to commands<br/>append same object to events
Agent->>Builder: next AgentToServer check-in
Builder->>Store: next_pending_command(...)
Builder->>Builder: build command/custom payload
Builder-->>Agent: ServerToAgent.command or custom_message or flags
Builder->>Store: mark_command_sent(...)
What gets stored
store.queue_command(...) does two things at once:
- appends a
CommandRecordtoClientRecord.commands - appends that same command record to
ClientRecord.events
That makes queue_command(...) the main history-producing API for command-like behavior.
What gets sent
ServerToAgentResponseBuilder._apply_command_intent(...) maps the queued command to an outbound payload:
- restart ->
ServerToAgent.command - force resync ->
ServerToAgent.flags customorcustom_command->ServerToAgent.custom_message
When the command is marked sent
The provider marks a queued command as sent only after a command payload was actually emitted:
- HTTP path:
app.pycallsmark_command_sent(...)afterhas_dispatched_command_payload(...) - WebSocket path: same rule
History is therefore created at queue time, not at send time.
Next-Action Flow
Some provider actions are not modeled as CommandRecord.
Examples:
- remote config delivery
- package-availability placeholder behavior
These use ClientRecord.next_actions and are consumed separately from commands.
sequenceDiagram
participant UI as Provider UI/API
participant Routes as app_routes_clients.py
participant Store as state.py
participant Builder as app_response_builder.py
participant Agent as OpAMP Agent
UI->>Routes: route-specific POST/PUT
Routes->>Store: set state needed for action
Routes->>Store: enqueue_next_action(...)
Note over Routes,Store: optional add_event(...)
Agent->>Builder: HTTP AgentToServer check-in
Builder->>Store: pop_next_action(...)
Builder->>Builder: _apply_next_action(...)
Builder-->>Agent: remote_config / connection_settings / error payload
Important transport detail
next_actions are currently popped only for the HTTP channel in build_response(...).
That means:
- queued commands can be dispatched on HTTP or WebSocket
next_actionssuch as remote config apply are currently HTTP-only
This matters when you add a new client-directed action. Decide whether it is a true queued command or an HTTP-only next action before implementing it.
History-Producing APIs
The main store APIs and their history behavior are:
| API | History behavior | Notes |
|---|---|---|
queue_command(...) |
Automatic | Preferred for true commands |
set_client_heartbeat_frequency(...) |
Automatic | Specialized mutation with built-in event creation |
add_event(...) |
Manual | Use when the route must create a history item explicitly |
enqueue_next_action(...) |
None | Must be paired with add_event(...) if UI history is required |
set_next_actions(...) |
None | Pure state mutation |
set_requested_config(...) |
None | Pure state mutation |
set_agent_identification(...) |
None | Route currently adds history manually |
set_pending_remote_config(...) |
None | Route currently adds history manually |
Current Provider Patterns
Unified command path
These already follow a clear command queue pattern:
POST /api/clients/<client_id>/commands- custom command submission from the provider UI through that same route
These are the best reference implementations for new command work.
Specialized but still history-safe
These do produce history, but not through queue_command(...):
PUT /api/clients/<client_id>/heartbeat-frequencyPOST /api/clients/<client_id>/identifyPOST /api/clients/<client_id>/remote-config
Two different subpatterns exist here:
- heartbeat: the state API itself appends history
- identify and remote config: the route mutates state, then calls
add_event(...)
One important implementation detail:
- the heartbeat route records state and history, but it does not currently enqueue
change_connectionson its own
Routes with no current history
These routes change client-directed state but do not currently add a history item:
POST /api/clients/<client_id>/actionsPOST /api/clients/<client_id>/config
When modifying these areas, decide explicitly whether the absence of history is intentional or accidental.
MCP Exclusion
For this guide, MCP activity is excluded from the history contract.
Why:
- MCP is an automation and tooling surface, not the canonical UI-facing activity model.
- Developers should not infer user-visible history requirements from MCP helpers.
- Future command additions should preserve a clean distinction between provider UI/API history semantics and MCP automation semantics.
Practical rule
When you add or refactor a provider-side action:
- design history behavior based on the provider UI/API route
- do not treat MCP tool usage as evidence that the action should appear in the client history
Current-code caveat
Some MCP helpers currently reuse command queue helpers that append history automatically.
That does not change the design guidance above. If you are normalizing behavior in the future, prefer to preserve or restore the rule that MCP-originated activity should not drive UI history expectations.
How To Add A New Command
Use this decision tree first.
flowchart TD
A["New provider-side client action"] --> B{"Is it a true queued command intent?"}
B -->|Yes| C["Use /commands-style flow"]
B -->|No| D{"Is it an HTTP-only next action?"}
C --> E["Create/extend CommandObjectInterface implementation if needed"]
C --> F["Queue with store.queue_command(...)"]
C --> G["Ensure response builder emits command/custom_message/flags"]
D -->|Yes| H["Store action-specific state"]
H --> I["enqueue_next_action(...)"]
I --> J{"Should UI history show it?"}
J -->|Yes| K["Call add_event(...) or use a history-producing state API"]
J -->|No| L["Leave as pure state mutation"]
D -->|No| M["Re-evaluate design; maybe it is not a command surface"]
Step 1: Decide the semantic category
Ask these questions:
- Will the action be represented as a command intent that should appear in
ClientRecord.commands? - Should it be dispatchable on both HTTP and WebSocket?
- Is there a natural
classifierandactionpair? - Should it appear in user-visible client history?
If the answer is yes to the first three, it usually belongs on the command queue path.
Step 2: For true commands, prefer the command queue path
Use or extend:
POST /api/clients/<client_id>/commandsqueue_command_from_payload(...)store.queue_command(...)ServerToAgentResponseBuilder._apply_command_intent(...)
For custom commands:
- add a command implementation under
provider/src/opamp_provider/command_implementations/ - rely on startup discovery in
commands.py - rely on wildcard custom routing rather than per-command builder registration
This is documented in more detail in Adding Your Own Custom Action.
Step 3: For next actions, be explicit about history
If the action is implemented with:
enqueue_next_action(...)set_pending_remote_config(...)set_next_actions(...)set_agent_identification(...)
then history will not happen automatically unless you add it deliberately.
Do not assume that queuing something for the client automatically creates a timeline event.
Step 4: Confirm send-channel behavior
Before shipping a new action, verify:
- should it dispatch on HTTP only?
- should it dispatch on WebSocket too?
- is it consumed from
commands,next_actions, or another state field?
If you use next_actions, the current implementation is HTTP-only.
Step 5: Add tests at the right layers
Recommended test layers:
- route test for validation and queueing behavior
- state/store test if new mutation/history behavior is introduced
- response-builder test for outbound protobuf shape
- end-to-end provider endpoint test covering queue and consume
- UI metadata test for custom commands if the command is user-selectable
History-specific assertions should verify the intended policy:
- command route -> history created automatically
- next-action route -> history only when explicitly added
- MCP-specific tests should not define UI-history expectations
Checklist For Developers
When adding or refactoring a provider-side command or client action:
- decide whether it is a
CommandRecordflow or anext_actionsflow - choose the transport expectation: HTTP-only or HTTP plus WebSocket
- decide whether the action should create a user-visible history entry
- if it should, use a history-producing API or add
store.add_event(...) - if it is MCP-only or MCP-originated, do not use it to define history rules
- add or update tests for queueing, dispatch, and history behavior
- link the change back to the custom-command docs when relevant
Prompt Template For Codex Or Similar Tools
Use the following prompt as a starting point when asking a coding tool to add or refactor a provider-side command:
Implement a new provider-side client action in this repo.
Before editing, read:
- docs/dev/server(provider)/provider_command_and_event_flow_guide.md
- docs/dev/adding_your_own_custom_action.md
- docs/dev/command_process_implementation_note.md
Requirements:
- Treat provider UI/API command behavior as the source of truth.
- Exclude MCP activity from the history requirements for this task.
- Decide whether the new behavior belongs on the CommandRecord path or the next_actions path.
- If it is a true command, prefer the existing /api/clients/<client_id>/commands flow and store.queue_command(...).
- If it is a next action, be explicit about whether it should create UI-visible history.
- Do not assume enqueue_next_action(...) or other state setters create history automatically.
- Verify whether the action should dispatch over HTTP only or over HTTP and WebSocket.
- Add tests for validation, queueing, dispatch shape, and history behavior.
Deliverables:
- code changes
- test updates
- a short explanation of which state/history path was chosen and why
Recommended Reading Order
For a new contributor:
- Read this guide first.
- Read Command Process Implementation Note for the lower-level command queue details.
- Read Adding Your Own Custom Action when the change involves a new custom command implementation.
- Read Consumer Custom Handlers if a new consumer capability handler is needed.
Summary
The provider does not have a single universal path for all client-directed actions.
queue_command(...)is the clean, history-producing path for true commands.next_actionsand other state setters are separate mechanisms and usually require explicit history decisions.- send behavior differs by channel: queued commands can go over HTTP or WebSocket, while
next_actionsare currently applied only on HTTP. - MCP activity should be excluded when defining or reviewing provider UI history behavior.
If you follow those rules first, the implementation choices in this area become much easier to make consistently.
OpAMP