vike
Desktop appStudio

The AI copilot

The Studio's chat pane sends a description to a model, compiles and backtests what comes back on held-out data, and shows a diff before anything touches the editor.

The chat pane is a thin front over one library loop. crates/vike-studio/src/chat.rs's ChatPane owns the transcript, the provider selector and the last result; the work happens in vike-ai's develop_strategy loop (crates/vike-ai/src/agent.rs) — generate, compile-validate, backtest out-of-sample, repair. The pane runs it the way the Run toolbar runs a backtest: ChatPane::send spawns a worker thread and hands back a receiver, and StudioState::poll drains it beside the run, sweep and walk-forward receivers.

What Send needs

Send is enabled only when four things hold at once: no chat round trip is already running, the selected provider has a key, the input is non-empty, and a data slice is selected in the picker. The last is not decoration — StudioState::start_chat_send says the model needs a (venue, symbol, interval) to backtest against, the same slice the Run toolbar uses (see Data and slices).

Providers and keys

crates/vike-ai/src/provider.rs's Provider offers two: Anthropic, the default, and Cerebras. Each needs one key — ANTHROPIC_API_KEY or CEREBRAS_API_KEY, the names chat.rs's key_env_var fixes for both the lookup and the "no key" error, so the failure message names the variable to set.

ChatApiKeys says where they come from: <project>/settings/secrets.env, the same store every venue credential lives in, resolved by the same walk. The pane itself opens no store and reads no environment — the desktop app resolves the keys from the credential map it already loads and hands them in as a parameter. And they are not venue credentials: absent, the pane offers no provider and Send stays disabled, and nothing about trading changes.

Keys are read once, at construction

The provider list is computed when the pane is built, not per frame. A key added to the store afterwards needs a restart — and a present-but-blank line (ANTHROPIC_API_KEY=) counts as present: the provider is offered and the request fails at the provider. The doc pins that deliberately rather than silently changing which providers the picker lists.

What the model is told

Two things precede every message. The system prompt, agent.rs's STRATEGY_SYSTEM_PROMPT, is the Rhai contract (the scripting tree's host surface): define fn on_bar(); read position(), price() and the host-bound indicators (NaN until warmed up); order with market(dir, qty) or buy/sell; declare sweepable knobs with param("x", default); submit through a submit_strategy tool carrying code and a one-line explanation. An unbound name raises on the first call rather than returning a plausible number.

Then chat.rs's grounding_preamble: the target slice — venue, symbol, interval — a note that the strategy is backtested on that instrument's out-of-sample data, and one valid template as a worked example. It deliberately does not enumerate the indicator registry. The doc records that the reason inverted: the host used to bind only three indicators; now it binds most of the registry, and what remains is a token budget — a fixed roster in every message crowds out the context specific to this chat.

The loop, and how it scores

develop_strategy scores only on the out-of-sample tail the model never saw (holdout_frac). A rejected submission goes back with the compile error or the reason it did not trade, and the model resubmits. max_repairs caps the number of submission attempts actually compiled and backtested — not the number of tool calls — and once the cap is reached every further submit_strategy is refused with a terminal message.

Reviewing the result

The transcript line is summary_of's: accepted, with the OOS Sharpe and trade count; not accepted after N attempts, with the problems; or an error. Below it the pane shows a result chip and a Review changes section — diff_rows, a line-level diff of the current editor buffer against the generated script. apply_result's doc states the contract: the diff comes first so Apply is a reviewed action rather than a blind clobber, and the write itself is still a whole-buffer replace once you accept. Until you press Apply to editor, the editor is untouched.

Memory across sessions

The loop has a ledger. crates/vike-ai/src/ledger.rs keeps two best-effort JSON files under the store root — ai_trials.json and ai_learnings.json — colocated with studio_strategies.json (see Saved strategies and workspace); the Studio resolves the path with LedgerPaths::under because the library reads no environment. Per agent.rs's ledger section it buys two things: prompt grounding, so the loop stops rediscovering duds it already rejected on this exact slice, and cross-session deflation — the deflated Sharpe ratio (Bailey and López de Prado, 2014) counts every prior accepted trial on the same (venue, symbol, interval), so a single-candidate chat that is accepted on a slice already carrying an accepted trial gets a real deflated_sharpe instead of the 0.0 a ledger-less call reports. Fewer than two trials in total — the first chat on a fresh slice included — leaves deflation undefined and the field at 0.0. With a ledger present the model is also offered a record_learning tool for notes future sessions read; without one it is not advertised.

The ledger is bounded — MAX_TRIALS 500 rows, oldest dropped first; equity curves kept on the newest MAX_CURVE_ROWS 50 accepted rows; MAX_LEARNINGS 200 notes of at most MAX_LEARNING_TEXT 1024 bytes — and best-effort: a missing file is empty, a corrupt one is empty plus a warning, a failed write is a warning. The loop never fails because the ledger is unavailable.

ChatOutcome's error arm covers the no-key case and a worker that panicked; both land in the transcript as a readable line rather than a silent stop. The Connect to Claude button at the bottom of the pane uses none of these keys and has its own page.

On this page