StScript assistant¶
stscript.assistant is a capability of the AI chat: a server-side behavior profile for
the script editor. It creates indicators and robots from a description, explains code or a
selected fragment, analyzes errors and logic, and proposes changes to the open document.
It is not a separate chat service and not a separate provider. The same methods are used — AIChatCreateConversation, AIChatSendMessage, AIChatGetRun — with a few new fields, plus two new methods: AIGetCapabilities and AIChatGetArtifact. A chat that does not send the new fields works exactly as before.
What it does not do. It never saves, runs or publishes a script, never enables autostart and never trades. There is no backtest, no profit estimate and no parameter optimization. Checking code on the server calls no trading method of the platform.
How a request flows¶
Editor AI module Model
│ AIGetCapabilities ──────────▶ runtimeId, referenceVersion, limits
│ AIChatCreateConversation ──▶ conversation with capability
│ AIChatSendMessage ─────────▶ checks: capability, schema, size, hash,
│ (editorContext snapshot) runtime, selection, declared kind
│ ◀────────────────────────── runId (queued) snapshot saved
│ reference search ──────────────────────▶
│ ◀── ai.run.progress ─────── candidate ◀────────────────────────────── code
│ parse → validate → compile → policy
│ diagnostics ───────────────────────────▶ repair
│ final check of the exact source
│ ◀── ai.run.completed artifact + message saved
│ AIChatGetRun ──────────────▶ text, artifact descriptor, analysis
│ AIChatGetArtifact ─────────▶ base + proposal + validation report
│ diff, local re-check, apply with undo (by the user)
1. Discover the capability¶
Call AIGetCapabilities once per session. Offer the mode only
when the capability is enabled, and send the runtimeId and referenceVersion it returns
with every request.
runtimeId identifies one immutable build of the StScript parser, validator, compiler and
standard library — the fingerprint of ion-interface/src/app/core/dsl/runtimeIdentity.js
(node scripts/stscript-runtime-identity.mjs prints it):
- take the pairs
[filename, source]forcore.js,stdlib.js,stdlib_math.js,stdlib_ta.js,stdlib_ui.js,validator.js, in exactly this order; - read each source as UTF-8 and normalize CRLF and CR to LF;
- SHA-256 over the UTF-8 bytes of
JSON.stringify(entries), lowercase hex; - prefix
stscript-dsl-sha256:.
The server computes the same value for the compiler package it checks with and publishes it
in discovery. If the editor ships another build, the server answers
AI_STSCRIPT_RUNTIME_UNSUPPORTED instead of silently checking with another version, and
the editor keeps sending source blocked until discovery confirms its exact runtime.
referenceVersion identifies the language reference the assistant uses. Both values are
opaque: never build them by hand.
2. Create a conversation¶
{
"title": "EMA indicator",
"accountLogin": 7777864,
"capability": "stscript.assistant",
"protocolVersion": 1
}
The server picks the agent of the brand and audience that supports the capability; the
interface does not keep an agentId. The capability is fixed for the conversation: a
regular chat never turns into a code session, and a code session does not accept questions
without a snapshot. Use a separate conversation for each.
AIChatGetConversations accepts capability to list only
editor conversations (stscript.assistant) or only regular ones (none).
3. Send a request with the editor snapshot¶
Every new question carries the current editor state, also for an empty document. Old code from the history never replaces it. If the user turned off sharing code with the assistant, do not send the request at all: the server never reads saved scripts behind the user's back.
{
"conversationId": "conv_…",
"requestId": "ef731e89-89e0-42d5-b806-316aad662a23",
"accountLogin": 7777864,
"content": "Add a second moving average with period 50",
"route": "/en/platform/scripteditor",
"capability": "stscript.assistant",
"protocolVersion": 1,
"operation": "modify",
"ui": {
"language": "en",
"platform": "desktop",
"supportedActions": ["stscript.editor.proposal.open"]
},
"editorContext": {
"schema": 1,
"runtimeId": "stscript-dsl-sha256:3de9a0c52570c0657861ab27d2ca22d3e46a59015e87043ae066c270cc236c99",
"referenceVersion": "reference-…",
"documentId": "a3cb73aa-f567-4105-b080-14cb172ef1c9",
"documentVersion": 12,
"scriptId": 42,
"language": "stscript",
"kind": "indicator",
"name": "EMA",
"source": "script(\"EMA\", kind=\"indicator\")\ninput length = 20\nma = ta.ema(close, length)\nplot(ma)\n",
"sourceHash": "sha256:…",
"selection": { "startLineNumber": 3, "startColumn": 1, "endLineNumber": 3, "endColumn": 27 },
"diagnostics": []
}
}
Operations¶
operation |
What the user wants | Proposal |
|---|---|---|
auto |
the assistant decides from the message, the code, the selection and the diagnostics; the document may be empty | only when the user asks to create or change code |
generate |
a new script from a description; the document may be empty | full new source |
explain |
an explanation of the code or of the selection | none |
analyze |
errors and possible logic problems | none; findings in analysis |
fix |
correct the errors with minimal changes | full corrected source |
modify |
change the code as asked | full new source |
When the task is ambiguous the assistant may answer with a clarifying question and no proposal.
Send auto when discovery lists it. The user does not choose an action; the assistant
decides it again for every message, and a question about the code never forces a change of
the code. What the answer actually did is returned as result.resolvedOperation (generate,
modify or fix when there is a proposal, otherwise explain or analyze) and stored
as the operation of the artifact. An explicit operation, including modify sent by a
client that does not know auto, is a hint as well: the user's message decides, and a
question gets an answer without a proposal.
editorContext¶
The schema is strict: an unknown field is rejected. The JSON Schema is published with the
module (skills/stscript-assistant/contracts/editor-context.schema.json in sct-ai).
| Field | Rule |
|---|---|
schema |
1 |
runtimeId, referenceVersion |
exactly the values from AIGetCapabilities |
documentId |
UUID of the editor tab or document |
documentVersion |
integer that grows on every change of the source. Correlation only — not a saved revision and not an authorization |
scriptId |
id of the saved script, or null for an unsaved document |
language |
stscript |
kind |
indicator or robot. For existing code the script(...) declaration wins; a contradiction is AI_STSCRIPT_KIND_MISMATCH |
name |
document name, up to 255 characters |
source |
the current unsaved code. Empty only for generate |
sourceHash |
hash of source, see below |
selection |
a Monaco range or null. The server derives the selected text from source itself |
diagnostics |
optional, up to 100 diagnostics of the editor. Treated as untrusted hints |
content stays the task description with its usual limit of 8 000 characters. Never put
the source into content.
Hash and positions¶
These rules must match byte for byte on both sides, otherwise every request fails with "hash does not match":
- Normalize line endings to LF (
\r\nand\rbecome\n) before hashing. Nothing else: no trim, no Unicode normalization, no added final newline. sourceHash = "sha256:" + lowercaseHex(SHA-256(UTF-8(source))).- The source limit is 64 000 UTF-16 code units —
string.lengthin JavaScript, where an emoji counts as two. The request body is limited to 1 MiB. - Positions follow Monaco: lines and columns start at 1, columns are UTF-16 code units, the
end of a range is exclusive.
nullmeans the parser gave no position; the server never invents one.
skills/stscript-assistant/contracts/fixtures/golden.json in sct-ai contains hash
fixtures (empty string, CRLF, lone CR, trailing spaces, Cyrillic, emoji, combining marks)
and a corpus of sources with the expected check result. Run the editor validator over the
same corpus to make sure both sides agree on status, kind, inputs and positions.
Idempotency¶
The answer is the usual { runId, messageId, state, idempotent }. Repeating the same
requestId with the same normalized payload in the same session, account and conversation
returns the existing run — retry after a timeout with the same key. The same key with a
different source, operation or text is AI_REQUEST_ID_CONFLICT, not a new answer.
4. Follow the run¶
The usual events arrive — ai.run.queued, started, delta, tool.started,
tool.completed, completed, cancelled, failed — plus ai.run.progress for the
stages of the code check:
{
"event": "module:event",
"type": "ai.run.progress",
"data": {
"runId": "run_…",
"conversationId": "conv_…",
"sequence": 8,
"phase": "validating",
"attempt": 1,
"maxAttempts": 3
}
}
phase is one of retrieving, generating, validating, repairing, finalizing. While
the run is active, AIChatGetRun returns the same object as
progress. Events never carry the source: after completed, read the run and the
artifact. Text deltas are an explanation for the user, not an instruction to apply
anything; the final result.content is authoritative because the explanation may change
after the check.
5. Read the result¶
result of AIChatGetRun keeps its usual fields and adds:
{
"capability": "stscript.assistant",
"protocolVersion": 1,
"operation": "auto",
"resolvedOperation": "modify",
"content": "Added EMA 50. Review the changes before applying.",
"artifacts": [{
"id": "art_…",
"type": "stscript.source_proposal",
"schema": 1,
"title": "EMA",
"kind": "indicator",
"validationStatus": "passed",
"canApply": true,
"sourceHash": "sha256:…"
}],
"analysis": {
"schema": 1,
"summary": "Added a second EMA without changing the first.",
"findings": [],
"referenceIds": ["ta.ema", "ui.plot"]
},
"actions": [{
"id": "act_…",
"type": "stscript.editor.proposal.open",
"version": 1,
"mode": "button",
"risk": "low",
"label": "Review changes",
"labelKey": "ai.stscript.reviewChanges",
"payload": { "artifactId": "art_…" },
"idempotencyKey": "run_…:stscript.editor.proposal.open:art_…"
}],
"sources": [{ "id": "ta.ema", "title": "ta.ema — exponential moving average", "type": "stscript.reference", "referenceVersion": "reference-…" }]
}
- One artifact at most per answer. The full code is loaded with AIChatGetArtifact; the run and the history carry only the descriptor.
- The kind of a proposal comes from its own
script(...)declaration, not from the open document: a robot proposed for an open indicator is checked by the robot validator and the robot policy, andbase.kindkeeps the kind of the document. canApplymeans the server accepted the proposal: it passed parse, validate, compile and policy for exactly thissourceHash. It is not a guarantee that the editor still holds the base code.- When no candidate passed after the repairs, the run is still
completed: the artifact hasvalidationStatus: "failed",canApply: falseand no action, and the text explains what is wrong. A technical failure of the model or of the validator, or a timeout, isfailedwith anerrorCode. analysis.findingsare logic remarks of the assistant (origin: "ai",phase: "analysis") in the diagnostic format. They are never mixed with compiler errors.- The action
stscript.editor.proposal.openonly opens the comparison. It is returned only whenui.supportedActionsdeclares it, and only for an applicable proposal. The artifact can be read without it: a client that sends noui(for example withsettings.ai.sendUIContext = false) opens the descriptor fromresult.artifactsitself. The first editor client does not execute AI actions at all and works from the descriptor.
History (AIChatGetConversation) returns the descriptors
in message.metadata.artifacts and the analysis in message.metadata.analysis. The user
message keeps metadata.editor — document id, version, kind, hash and size of the snapshot,
not the code. Polling and reconnecting return the same artifact ids; nothing is generated
again.
6. Apply a proposal¶
The frontend is responsible for these steps:
- Show which document the chat is attached to and that its code is sent.
- Send the current snapshot with every request and increase
documentVersionon every edit. - Render the text, the diagnostics, the reference links and the artifact descriptor.
- Load the full artifact and build the difference from
base.sourceandproposal.source. The first release always returns the full new source — no patches. - Before applying, check the account,
documentId,runtimeId, that the editor still holdsbase.sourceHash, and thatproposal.sourcehashes toproposal.sourceHash. Re-check the proposal with the local validator. - If the code changed meanwhile, show a conflict and offer a new request with the current snapshot. There is no automatic merge.
- Apply only by an explicit user action, as one Monaco edit that can be undone; this increases the document version.
- Do not apply the same artifact to the same version twice, and never run actions from the history automatically.
- Save and Run stay separate editor operations. Changing the code does not change a robot that is already running.
An empty document follows the same rules: scriptId: null does not allow applying a
proposal to any other open tab.
Diagnostics¶
{
"code": "STSCRIPT_PARSE_ERROR",
"severity": "error",
"origin": "server",
"phase": "parse",
"message": "Unexpected token '/' at start of statement",
"range": { "startLineNumber": 2, "startColumn": 1, "endLineNumber": 2, "endColumn": 2 },
"referenceIds": ["language.syntax"]
}
| Field | Values |
|---|---|
severity |
error, warning, info |
origin |
client, server, ai |
phase |
parse, validate, compile, policy, runtime, analysis |
range |
Monaco range or null |
referenceIds |
only ids of the reference of the current referenceVersion |
Codes of the server check:
| Code | Phase | Meaning |
|---|---|---|
STSCRIPT_EMPTY_SOURCE |
parse | no code |
STSCRIPT_PARSE_ERROR |
parse | the parser rejected the code; the range points into the original source |
STSCRIPT_DECLARATION_ERROR |
validate | missing or wrong script(...) declaration |
STSCRIPT_KIND_MISMATCH |
validate | the declared kind differs from the expected one |
STSCRIPT_VALIDATION_ERROR |
validate | unknown function or variable, assignment to an input, a reserved name, trade.*/log outside an event handler, a repeated handler, a loop or syntax tree over the runtime limits, … (no position). The message keeps the runtime code, e.g. SCRIPT_LOOP_LIMIT, SCRIPT_AST_LIMIT, SCRIPT_ACTION_REQUIRES_EVENT |
STSCRIPT_COMPILE_ERROR |
compile | the compiler rejected the code, for example a UI call in a robot |
STSCRIPT_POLICY_DRYRUN_REQUIRED |
policy | a robot without input dryRun = 1 |
STSCRIPT_POLICY_LIVE_TRADING |
policy | dryRun or simulate switched off |
STSCRIPT_POLICY_AUTOSTART |
policy | input autostart enabled |
STSCRIPT_POLICY_POSITION_SCOPE |
policy | positionScope widened to account |
STSCRIPT_POLICY_AST_LIMIT |
policy | more than 10 000 syntax nodes |
STSCRIPT_CHECK_LIMIT |
parse | the check exceeded 5 s or 256 MiB |
Validation report¶
{
"schema": 1,
"runtimeId": "stscript-dsl-sha256:3de9a0c52570c0657861ab27d2ca22d3e46a59015e87043ae066c270cc236c99",
"referenceVersion": "reference-…",
"sourceHash": "sha256:…",
"status": "passed",
"checks": { "parse": "passed", "validate": "passed", "compile": "passed", "policy": "passed" },
"execution": { "status": "not_run", "reason": "NOT_ENABLED" },
"diagnostics": [],
"limitations": ["NO_BACKTEST", "NO_LIVE_EXECUTION"]
}
A check is passed, failed or not_run; the overall status is passed, failed or
unavailable. passed requires parse, validate, compile and policy to pass for exactly
sourceHash. The code is never executed in this release, which execution states
explicitly.
How the server checks code¶
- The check uses the same parser, validator and compiler as the editor, taken from
ion-interfaceand pinned byruntimeId. There is no second parser and no check by regular expressions or by the model's word. - The path repeats the editor: parse → mandatory
script(...)declaration → validation for the declared kind → compile. - Each check runs in a separate process with no environment, no credentials and no network connections of the service, limited to 5 seconds and 256 MiB. The compiled function is constructed to verify it, never called.
- The policy is checked on the syntax tree, not by searching strings: a robot proposal must
declare
input dryRun = 1, must not switchdryRunorsimulateoff, must not enable autostart and must not widenpositionScopetoaccount. Existing live code can be explained and analyzed, but an applicable proposal never keepsdryRun = 0. - After the model's repairs the final source is checked again, independently of what the model reported. Up to three candidates per answer: the first plus two repairs.
Errors¶
New request errors keep the usual body { "error": "CODE", "message": "…" }. Some codes
also carry structured data of the module error — reason for AI_CAPABILITY_DISABLED,
requested and supported for AI_STSCRIPT_RUNTIME_UNSUPPORTED, expectedSourceHash for a
hash mismatch; whether it reaches the client as details depends on the platform router.
The interface must work from the code alone.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | AI_STSCRIPT_CONTEXT_REQUIRED |
no editorContext |
| 400 | AI_STSCRIPT_CONTEXT_INVALID |
wrong schema, operation, range, hash, or an empty source for an operation that needs code |
| 400 | AI_STSCRIPT_KIND_MISMATCH |
kind contradicts the declaration of the existing code |
| 403 | AI_CAPABILITY_DISABLED |
the capability is not available in this scope (details.reason) |
| 404 | AI_ARTIFACT_NOT_FOUND |
the artifact does not exist or is not available to this session |
| 409 | AI_CAPABILITY_MISMATCH |
the request does not match the conversation's capability or protocol |
| 409 | AI_REQUEST_ID_CONFLICT |
the same requestId with a different payload or from another scope |
| 409 | AI_STSCRIPT_RUNTIME_UNSUPPORTED |
no exactly matching compiler and reference (details.supported) |
| 413 | AI_STSCRIPT_SOURCE_TOO_LARGE |
the source or the request body exceeds the limit |
| 429 | AI_RATE_LIMITED |
another code request of the session is running, or an hourly quota is reached |
| 503 | AI_STSCRIPT_VALIDATOR_UNAVAILABLE |
the check cannot be performed now |
Existing errors such as AI_CONVERSATION_BUSY, AI_BUDGET_EXCEEDED and AI_ACCESS_DENIED
keep their meaning. Wrong code in a fix or analyze request is the task itself, not a
request error.
After a run is accepted, failures arrive in errorCode / errorMessage of the run:
AI_STSCRIPT_VALIDATOR_UNAVAILABLE, AI_OUTPUT_INVALID (the model did not keep the
format), AI_OUTPUT_TRUNCATED (the model ran out of output tokens; a partial script is
never proposed), AI_RUN_TIMEOUT, AI_CONTEXT_TOO_LARGE, AI_STSCRIPT_RUNTIME_UNSUPPORTED.
Reading the run still answers 200.
Limits¶
| Limit | Default |
|---|---|
| Run time | 180 s |
| Repairs after the first candidate | 2 (3 candidates) |
| Artifacts per run | 1 |
| Tool calls per run | 12 |
| Source | 64 000 UTF-16 code units |
| Request body | 1 MiB |
| Client diagnostics | 100 |
| One check | 5 s, 256 MiB |
| Active code requests per session | 1, plus hourly quotas per session and per account |
All limits are configurable in the [stscript] section of the module configuration.
A brand that does not agree to send source code to external model providers can switch the
capability off for itself; it then reports enabled: false.