Interface Actions¶
The assistant can do more than explain: it can ask the interface to switch the theme, open
a screen or select a symbol on the chart. Those requests arrive in the actions array of
an answer, next to content and blocks.
{
"id": "act_9f2c1b7d4e5a6b80",
"type": "platform.theme.change",
"version": 1,
"mode": "auto",
"risk": "none",
"payload": { "theme": "light" },
"idempotencyKey": "run_55b…:platform.theme.change:1a2b3c4d"
}
The list of action types is closed and lives in the service, not in the model. The
model picks a type from an enumeration and fills payload against a schema; mode,
risk and version come from the catalogue and cannot be overridden. The reason is
plain: sooner or later the model would mark "turn the robot on" as auto, and the
interface would do it without asking anyone. Risk is a property of the action, not of how
the question was phrased.
The contract¶
| Field | Meaning |
|---|---|
id |
unique id of the action inside this answer, act_… |
type |
stable machine-readable name, never renamed after release |
version |
payload schema version; absent means 1 |
mode |
auto, confirm or button — how the interface applies it |
risk |
none, low, medium, high — for the UI and for audit |
label / labelKey |
button caption and a key for your own translation; absent for auto |
payload |
action parameters |
idempotencyKey |
key that prevents the same action from being applied twice |
Execution modes¶
mode |
What the interface does | Used for |
|---|---|---|
auto |
applies it right after the answer arrives | theme, panel, symbol, timeframe — visible and instantly reversible |
confirm |
asks the user first | account switch, layout, language, robots, order preparation |
button |
renders a button under the message | navigation and follow-ups |
What the service validates¶
The interface must check its own whitelist, but it cannot be the only guard: payload is
filled by a language model.
- type — present in the catalogue and allowed for this session type;
- payload — against a strict JSON Schema with
additionalProperties: false. An extra field is either a hallucination or an attempt to override the mode; - route — internal paths only. A scheme (
javascript:,https:),//host,..and control characters are always rejected; a brand can narrow it further to a list of prefixes in[actions] routes; - account — a client is never offered someone else's account, not even as a button: seeing it, the person would assume the account is theirs.
A rejected action is returned to the model with the reason rather than dropped silently,
so it can correct itself or tell the user what it cannot do. The answer text never depends
on it: content is written to make sense without any action.
Idempotency¶
Every action carries idempotencyKey, built as <runId>:<type>:<payload hash>. Keep the
applied keys for the session and never apply the same key twice — a page reload or a
repeated AIChatGetRun poll must not switch the theme again.
The same rule answers what to do with history: actions from old messages are not
applied. GET ai/chat/conversation returns them in metadata.actions so a button under
an old answer stays clickable, but an auto action must never fire while scrolling the
conversation.
The catalogue¶
AIGetActions (GET ai/chat/actions) returns the same information in machine-readable
form, so the interface does not keep the list of types in two places.
{
"protocol": 1,
"enabled": true,
"maxActions": 3,
"rows": [
{
"type": "platform.theme.change",
"version": 1,
"mode": "auto",
"risk": "none",
"offered": true,
"description": "Switch the interface theme…",
"payloadSchema": {
"type": "object",
"properties": { "theme": { "type": "string", "enum": ["light", "dark", "system"] } },
"required": ["theme"],
"additionalProperties": false
}
}
],
"count": 26,
"offered": 12
}
offered is the difference between "described by the protocol" and "generated today".
Rollout stages¶
Twenty-six types are defined and twelve are offered to the model. Every action carries a stage, ordered by risk rather than by convenience:
| Stage | What it covers |
|---|---|
| 1 | what the screen looks like: theme, tab, panel, chart symbol and timeframe, Market Watch, preparing the order form |
| 2 | language, layout, indicators, account and deal cards, closing a position by ticket and partial close |
| 3 | switching the active account, opening a robot, opening a position |
| 4 | turning a robot on, closing all positions |
The limit is [actions] maxStage (default 1) and moves when the interface can execute
the previous stage. An explicit [actions] types overrides the stage, so a brand can
switch on one action without moving the whole step.
Stage 1 in full:
| Type | Mode | Payload |
|---|---|---|
platform.theme.change |
auto |
theme: light | dark | system |
platform.route.open |
button |
route (internal path), label? |
platform.panel.open |
auto |
panel: orders | positions | chart | ai-agent | settings | scripts |
chart.symbol.select |
auto |
symbol |
chart.timeframe.change |
auto |
timeframe: M1 … MN1 |
orders.open |
button |
tab: open | history | pending |
The rest wait for handlers in the interface. Offering all eighteen to the model would read
as an invitation to use them, and half the answers would turn into buttons nobody can
press. The list grows through [actions] types in the service config — no code changes.
Later stages, already described by the protocol: platform.language.change,
platform.layout.change, chart.indicator.add, chart.indicator.remove,
account.details.open, order.ticket.open, notifications.settings.open,
trading.position.close, trading.position.closePartial (stage 2); account.switch,
scripts.robot.open, trading.position.open (stage 3); scripts.robot.toggle,
trading.position.closeAll (stage 4).
What this interface can execute¶
ui.supportedActions is the list of handlers your build of the terminal actually has.
What the model is offered is narrowed to the intersection with the catalogue: there is
more than one version of the interface in the field and they do not update together, so
without it the assistant would promise things an older build cannot do. The list can
only narrow. Anything outside the catalogue or beyond the current stage never appears,
however much the interface claims.
Interface state¶
AIChatSendMessage accepts an optional ui block:
{
"conversationId": "cnv_…",
"content": "switch to the light theme",
"ui": {
"theme": "dark",
"language": "ru",
"platform": "desktop",
"route": "/terminal/chart",
"activePanel": "ai-agent",
"activeTab": "trade-chart",
"activeSymbol": "EURUSD",
"timeframe": "H1",
"marketWatchSymbols": ["EURUSD", "XAUUSD"],
"openOrderTickets": [123456, 123457],
"supportedActions": ["platform.tab.switch", "marketWatch.symbol.add"]
}
}
Everything works without it; with it the assistant stops offering to enable a theme that is already on — which reads as a malfunction rather than politeness. Every field is optional on its own: send what you know.
marketWatchSymbols and openOrderTickets make the clarifying question specific — "you
have two EURUSD positions, which one?" instead of "give me the ticket". None of it is
treated as authoritative: a position can close between the question and the answer, and
the interface validates everything anyway. supportedActions is the one field that
changes what the assistant may offer, and only by narrowing it.
What an action can never be¶
- There is no executable code. The protocol has no field for JavaScript, CSS, DOM
selectors, store commands or arbitrary endpoints. The only free-form field is
payload.route, and it is validated. - Trading never starts with the assistant. Opening and closing a position exist in
the protocol, but always
confirm, always on an explicit instruction with the direction and the volume named by the person, and never as a conclusion from market analysis. That is a product boundary rather than a technical one: an assistant that suggests buying is giving investment advice on behalf of the broker. A test enforces it — notrading.*action can havemode: auto. In stage 1 the only trading action available is preparing the form. - No more than three actions per answer (
[actions] maxActions). The fourth is refused with a suggestion to explain the rest in words.
Handling on the interface side¶
- Keep a map of handlers for the types you implement. Ignore an unknown
type, an unsupportedversionand an action whose payload does not match what you expect — the message is still shown, becausecontentstands on its own. - Apply
autoonce, keyed byidempotencyKey, and only for a freshly completed run. - For
confirm, show what exactly will happen —type,payloadandriskare enough to phrase it. - For
button, renderlabel(or your translation oflabelKey) under the message. - Log what you did with each action: received, ignored, applied, failed, confirmed or rejected by the user. The service logs its own half — which action it produced, with what payload, mode and risk, for which run and session.
Button captions¶
button and confirm actions carry label — the caption — and labelKey for your own
translation. On safe buttons the caption is written by the model in the language of the
conversation ("Показать историю" rather than "Open screen"); on risky confirmations it
comes from the catalogue, because a self-written caption on a "turn the robot on" button
is exactly what should not happen. If you have a dictionary, prefer labelKey.
An action never replaces data¶
When the user asks what their history, positions or balance are, the assistant fetches the
data and answers with the figures; the action is offered in addition, never instead. This
is enforced rather than requested: orders.open, order.ticket.open and
account.details.open are refused until a data tool has run in that answer, so a question
about your own data always comes back with a block and a button. The
same rule says that button and confirm have not happened yet, so the answer never
claims the screen was already opened. Both are instructions the model reads before every
answer — worth knowing, because it explains why a data question returns blocks and an
action.
Trading actions¶
Opening and closing a position is the only thing that spends a person's money, so the rules are stricter and all of them live in the service:
| Action | What the person must supply |
|---|---|
trading.order.prepare |
fills the form; no order is sent |
trading.position.close |
the ticket of one position |
trading.position.closePartial |
the ticket and the volume |
trading.position.open |
symbol, side and volume — all named by the person |
trading.position.closeAll |
an explicit "close everything" |
Closing everything is a separate type, not a scope field on the ordinary close as
the specification proposed. One type would then carry two different risks — closing one
deal and closing them all — and an optional field that changes the scale of the
consequences is exactly what a model, and a person reading a confirmation dialog, get
wrong. With a separate name the scale is visible in the modal and in the audit trail.
What the service will not do: propose a trading action on its own, derive one from
market analysis, answer "what should I buy" with an action, or guess a volume or a side.
A missing parameter produces a clarifying question and an empty actions.
When no action comes¶
An ambiguous request is a clarifying question, not a button. "Change the theme" without
saying which one returns actions: [] and a question in the text. That instruction is
part of what the model reads before every answer.
Actions can be switched off entirely with [actions] enabled = false: the model is not
offered the tool at all, and knowledge-base buttons stop reaching answers.