Skip to content

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: M1MN1
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 — no trading.* action can have mode: 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

  1. Keep a map of handlers for the types you implement. Ignore an unknown type, an unsupported version and an action whose payload does not match what you expect — the message is still shown, because content stands on its own.
  2. Apply auto once, keyed by idempotencyKey, and only for a freshly completed run.
  3. For confirm, show what exactly will happen — type, payload and risk are enough to phrase it.
  4. For button, render label (or your translation of labelKey) under the message.
  5. 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.