# Lumen Charts documentation

Public integration documentation for a proprietary library. Runtime access is separately authorized. Exported API signatures do not imply that all legacy capabilities are supported; preserve compatibility notes when using these documents.

<!-- Public documentation export; not library implementation source. -->
# AI integration overview

AI integration is built on the Lumen Charts automation API, which can serve ordinary JavaScript, custom workflows or an AI assistant. Your application chooses the model provider, transport, storage and commercial access policy.

The SDK is open source under the Apache License 2.0 ([source on GitHub](https://github.com/lumenchartshq/chart-automation)), and every Lumen Charts build includes the chart-side runtime it drives. There is no separate add-on to buy.

## Quickstart

Open a session for a chart, add your own command and hand the tools to your model:

```ts
import { createAutomationTools, defineCommand, field } from '@lumencharts/chart-automation/sdk';

// Inside the widget's ready callback.
const sdk = await widget.activeChart().openAutomation({
  policy: { effects: ['read', 'mutation'] },
});

await sdk.drawings.apply({
  operations: [{ action: 'create', type: 'horizontal_line', points: [{ price: 182.5 }] }],
});

sdk.extensions.register(defineCommand({
  name: 'acme.lastClose',
  effect: 'read',
  description: 'Return the latest loaded close.',
  input: {},
  output: { close: field.nullable(field.number()) },
  async execute(_input, context) {
    const { rows } = await context.execute('data.read', { seriesId: 'main', from: 0, to: Date.now(), limit: 10_000 });
    return { close: (rows.at(-1) as { close?: number } | undefined)?.close ?? null };
  },
}));

const tools = createAutomationTools(sdk);   // name, description, parameters, execute(toolCallId, input)
const system = sdk.instructions();          // guidance from registered commands

// When the view is removed (removing the chart also closes the session):
sdk.close();
```

`openAutomation` loads the optional automation asset, installs the native drawing, study and settings commands and wires an exact-chart PNG capture. Every `request` argument is optional; pass one when work must stay fenced to the chart state a caller saw earlier, as model tools do.

## Product boundaries

| Part | Responsibility | Loading |
| --- | --- | --- |
| Core chart runtime | Charts, datafeed integration, native editing, persistence and Undo | Widget startup |
| Automation runtime | Native command adapters and the session behind `openAutomation`, included in every build | First `openAutomation()` or `widget.loadAutomation()` |
| Automation SDK (open source) | Sessions, typed command facade, data/capture ports, policies, extensions, model tools and remote sessions | Opt-in application import from npm |
| Optional Pine | Compile and execute scripts and display their outputs | Separate Pine configuration and assets |
| Host application | Model calls, prompts, uploads, accounts, remote storage and entitlements | Supplied by the integrator |

The core does not bundle the automation implementation. Loaded-bar reads and local screenshots are native capabilities; `createNativeChartAutomation` uses them without loading the automation runtime. Native persistence and ordinary Undo also work without it.

Pine executes a script over market data and produces calculated outputs. Automation orchestrates chart operations and accesses data or images through explicit ports. It does not implicitly compile Pine, generate scripts or connect to an AI service. Hosts can compose these capabilities.

## Access and prerequisites

Install the SDK from npm. It ships as one package built to ESM with type declarations: `@lumencharts/chart-automation`, with the entry points `/sdk`, `/sdk/testing`, `/contracts` and `.`.

```bash
npm install @lumencharts/chart-automation
```

`lumen_charting_library.d.ts` names SDK types only through this package, so install it before type-checking against the chart. Update the SDK and the chart runtime together. `openAutomation` itself needs no SDK import; the SDK supplies `defineCommand`, `field`, model tools and remote sessions.

The surface has two supported tiers:

| Tier | What it covers | Stability |
| --- | --- | --- |
| Stable | `chart.openAutomation()` and `@lumencharts/chart-automation/sdk`: sessions, `defineCommand`, `field`, `createAutomationTools`, remote sessions and the domain types they name. `@lumencharts/chart-automation/sdk/testing` for unit tests. | Supported for integrators. |
| Advanced | `createChartAutomation`, `createNativeChartAutomation`, `installNativeAutomationCommands` and the native owner factories (`createDrawingDocument`, `createStudyDocument`, `widget.createWorkspaceCommands`). The package root exports the drawing, study, settings and workspace contracts and validators these owners use. | Supported, but you own each owner's lifetime, persistence and Undo. |

Product-specific adapters, prompts and model tools belong in your application; none ship in this package.

The automation runtime is a separate file so charts that never use automation never download it. It needs no extra licence: every edition includes it. See [installation and hosting](https://docs.lumencharts.com/charting-library/getting-started/installation).

## Integration path

1. Read and capture the chart: [local data and capture](https://docs.lumencharts.com/charting-library/automation/data-and-capture).
2. Change the chart and connect a model: [native commands and model tools](https://docs.lumencharts.com/charting-library/automation/commands-and-tools).
3. Add your own commands, data and persistence: [host adapters and extensions](https://docs.lumencharts.com/charting-library/automation/extensions).
4. Run the model on a server: [server-side models](https://docs.lumencharts.com/charting-library/automation/remote).

Use `sdk.capabilities()` to discover installed, permitted operations. Unavailable command families reject with `unsupported`; a method existing in the TypeScript facade does not mean its native owner is installed.

---
Documentation page: https://docs.lumencharts.com/charting-library/automation/


---

<!-- Public documentation export; not library implementation source. -->
# Local data and capture

Open a session after widget readiness. Reads and capture need only the `read` effect:

```ts
// Run inside the widget's ready callback.
const chart = widget.activeChart();
const sdk = await chart.openAutomation({ commands: [], policy: { effects: ['read'] } });

const series = sdk.data.series();
const range = chart.getVisibleRange();
if (range && series.find(item => item.id === 'main')?.readiness === 'ready') {
  const result = await sdk.data.read({
    seriesId: 'main',
    from: Math.round(range.from * 1000),
    to: Math.round(range.to * 1000),
    limit: 2000,
  });
  console.log(result.rows, result.coverage);
}
const image = await sdk.capture.image();
console.log(image.blob, image.width, image.height);

// On component teardown (chart removal also closes the session):
sdk.close();
```

`commands: []` installs no native command families. Each call above uses a fresh request for the current chart; pass `sdk.request()` captured earlier to reject results from a chart that has since changed.

To avoid downloading the automation runtime for read-only use, bundle the SDK and create the session yourself. Keep the chart and screenshot index paired:

```ts
import { createNativeChartAutomation } from '@lumencharts/chart-automation/sdk';

const sdk = createNativeChartAutomation({
  chartId: 'primary',
  chart: widget.workspaceChart(0),
  capturePng: () => widget.takeClientScreenshot({ chartIndex: 0, strictCapture: true }),
  policy: { effects: ['read'] },
});
```

`capturePng` may return either a PNG data URL or a PNG `Blob`. A Blob is validated from its header only, so it avoids base64 decoding for large charts.

## Time, fields and coverage

SDK timestamps use integer UTC milliseconds. The native visible-range API uses seconds, hence the conversion above. Inside a command, read through `context.execute('data.read', input)` so the read shares the command's request, deadline and target fence. Main-series data is raw OHLCV, not transformed display candles. Open, high, low and close are required finite numbers; volume may be omitted.

Every discovered series declares its numeric fields, units and readiness. Fields are required and non-null by default. `optional: true` permits omission; `nullable: true` permits explicit `null`. Rows must contain an integer `time` and only declared fields. Timestamps must be unique, increasing and inside the requested range. Descriptors are snapshotted before asynchronous work so later provider changes cannot alter validation for a pending read.

The native adapter exposes main candles. Comparison, study and Pine output series require a host data adapter; they are not automatically extracted from arbitrary indicator buffers. Pending series reject local reads with `busy`; failed series reject with `unavailable`.

Reads never fetch missing history. An oversized matching window rejects instead of silently returning a partial slice. `coverage.complete` means the requested window is inside the reported loaded bounds; it does not promise a candle for every clock interval or exhaustive market history. `requestHistory` is a separate, optional host operation that requires the `external` effect.

## Capture and image transport

Capture returns a PNG Blob with dimensions, target, capture time, visible range and revision. The revision is a session change counter, not a persisted document version. Capture rejects stale work if the chart selection, data or visible range changes while it is pending.

The current capture scope is the exact chart at native dimensions. Cropping, custom dimensions, layer filtering and image editing are not SDK options. Your application can process the returned image separately. Uploads, attachment identities, retention and provider-specific image formats belong to that application.

## Events and cleanup

```ts
const unsubscribe = sdk.events.subscribe(event => {
  console.log(event.type, event.target, event.revision);
});
// Release when the observer is no longer needed.
unsubscribe();
```

Events include `selection`, `data`, `study-ready` and `disposed`. Data notifications are coalesced within a microtask; selection and disposal are immediate. A `study-ready` event first delivers any queued data notification, so events arrive in commit order. Payloads are immutable. The native session destroys itself when its chart is removed; explicit `destroy()` is also safe to call repeatedly.

[Add native commands and model tools →](https://docs.lumencharts.com/charting-library/automation/commands-and-tools)

---
Documentation page: https://docs.lumencharts.com/charting-library/automation/data-and-capture


---

<!-- Public documentation export; not library implementation source. -->
# Native commands and model tools

## Open a session with native commands

```ts
const sdk = await widget.activeChart().openAutomation({
  commands: ['drawings', 'studies', 'settings'], // the default
  policy: { effects: ['read', 'mutation'] },
});
console.log(sdk.capabilities().commands.map(command => command.name));
```

`openAutomation` requests `assets/chart-automation.js` relative to the configured library base on first use. Creating a widget alone never requests it. Concurrent loads are deduplicated; failed loads can be retried, and a loading failure leaves the ordinary chart usable. The `ai_commands` widget option does not load assets or grant a commercial entitlement, but it still limits what a session can install. A family is installed only when `commands` requests it **and** the widget allows it: with `ai_commands.chartSettings` off, `settings.*` stays `unsupported`; with study commands off, `studies.apply` is absent; with inspection off, the `inspect` reads are absent. Check `sdk.capabilities().commands` rather than assuming a requested family is present. Owners you pass in through `owners` are used as built, so the widget option does not apply to them.

| Family | Commands | Behavior |
| --- | --- | --- |
| Drawings | `drawings.context`, `list`, `apply`, `groups`, `inspect` | Drawing batches with native receipts and Undo; group pages; exact-ID settings |
| Studies | `studies.context`, `list`, `apply`, `inspect` | Built-in study changes with expected revisions; exact-ID inputs and styles |
| Settings | `settings.inspect`, `apply` | Validated chart and scale settings |
| Workspace | `workspace.context`, `apply` | Pass `workspace: widget.createWorkspaceCommands(options)`; you keep ownership |

`capabilities.query` is installed whenever at least one of the drawing, study or settings families is; a session with `commands: []` has none. Group and inspection reads appear only when the running chart supports them. Closing the session destroys the drawing, study and settings owners it created, never owners you passed in (see below); chart removal closes the session.

Built-in facade inputs and results have concrete types. Drawing and group pages allow up to 100 entries and study pages up to 20. Native parsers enforce operation limits and required fields. Capability discovery exposes supported fields and choices; do not assume every drawing setting is writable.

Mutations preserve native Undo and receipts. They do not save remotely unless you supply a `persist` hook (see [extensions](https://docs.lumencharts.com/charting-library/automation/extensions#durable-saves)). There are no transactions across commands or services.

### Keep one owner per chart

Each native family keeps its own lease, Undo history and persistence revision in one owner. Use one mutating owner per chart per family: two drawing owners on the same chart would hold separate leases and Undo stacks, so their edits could interleave unsafely. If you already hold an owner, for example one from `createDrawingCommands` or `createDrawingDocument` that saves your documents, pass it in:

```ts
await widget.loadAutomation();
const chart = widget.activeChart();
const drawings = chart.createDrawingCommands(binding); // your existing owner

const sdk = await chart.openAutomation({ owners: { drawings } });
const receipt = await sdk.drawings.apply({ operations: [{ action: 'create', type: 'horizontal_line', points: [{ price: 182.5 }] }] });
drawings.undo(receipt.operationId); // SDK changes share the owner's Undo history
sdk.close(); // `drawings` stays alive; you destroy it
```

`owners` accepts `drawings`, `studies` and `settings`. An adopted owner is used as-is and the session never destroys it. Families without an adopted owner are built and destroyed by the session as usual. When you also pass `commands`, every adopted family must be listed in it, or opening rejects with `invalid_input`. An adopted drawing owner also supplies the session's binding.

A second mutating session on the same chart must reuse the same owners rather than opening with fresh ones. Sessions that share an owner can run side by side and close independently.

Hosts that construct owners themselves can still call the lower-level factories (`createDrawingCommands`, `createStudyCommands`, `createChartSettingsCommands`) after `widget.loadAutomation()` and connect them with `installNativeAutomationCommands(sdk, owners)`.

## Model tools

```ts
import { createAutomationTools } from '@lumencharts/chart-automation/sdk';

const tools = createAutomationTools(sdk, {
  request: toolCallId => sdk.request({ requestId: toolCallId }), // the default
  imageResult: capture => uploadForMyProvider(capture.blob),     // enables capture_image
  use: [logCalls, confirmMutations],                             // middleware, outermost first
});
// Each tool: name, commandName, description, parameters, effect, concurrency, execute(toolCallId, input).
```

Translate each tool into your provider's format; the adapter has no model client, prompts, budgets or conversation store.

| Option | Purpose |
| --- | --- |
| `request` | Request envelope per call. Defaults to the call ID and the current chart. |
| `describe` | Replaces a tool's `description` or `parameters`, e.g. to keep wording you have evaluated. Runs before `include`; return `undefined` to keep the derived tool. Input is still parsed by the command's own schema. |
| `include` | Keep only tools for which it returns `true`, e.g. `tool => tool.effect === 'read'`. |
| `toolPrefix` | Prepended to derived names, e.g. `chart_` for `chart_drawings_apply`. |
| `toolNames` | Exact names by command, e.g. `{ 'drawings.apply': 'apply_chart_drawing_batch' }`. |
| `use` | Middleware `(call, next) => result` for logging, confirmation, budgets or result shaping. Throw a `ChartCommandError` to refuse. |
| `imageResult` | Converts a capture for your provider. Capture is advertised only when supplied. |
| `errorResult` | Shape of a failed call. Defaults to `{ error: code, details? }`, where `details` names the rejected field path and a fixed reason; never the error message. `null` rethrows. |

Tool names are snake_case: `drawings.apply` becomes `drawings_apply` and `navigation.searchSymbols` becomes `navigation_search_symbols`. The SDK ports are `data_series`, `data_read`, `data_request_history` and `capture_image`. Names must match `[a-z][a-z0-9_-]{0,63}`; a duplicate name rejects with `operation_conflict`.

Each tool reports its `effect` and `concurrency`. When a model asks for several calls in one step, run `shared` calls together and each `exclusive` call alone in call order. Reads default to `shared`; mutations and external calls default to `exclusive`.

Command `examples` are appended to that command's tool description as compact JSON lines (up to 2,000 characters), the same for local and remote sessions; commands without examples keep their description unchanged. `sdk.instructions()` joins the `instructions` of permitted commands for your system prompt. Tool lists are snapshots: rebuild them when `sdk.capabilities().revision` changes.

```ts
import { ChartCommandError, type AutomationToolMiddleware } from '@lumencharts/chart-automation/sdk';

const confirmMutations: AutomationToolMiddleware = async (call, next) => {
  if (call.tool.effect === 'mutation' && !(await askUser(`Allow ${call.tool.name}?`))) throw new ChartCommandError('cancelled');
  return next();
};
```

## Policy and request lifetimes

Configure allowed effects (`read`, `mutation`, `external`) and optionally an exact command-name allowlist when opening a session. `data_series` follows the `data.read` permission. Defaults are eight concurrent requests, a 30-second deadline and 256 KiB JSON payloads. Finite ceilings also bound rows, series, fields and images; inspect `capabilities().limits` for effective values.

Omitted requests get a fresh ID and the current target. Explicit requests must use a unique ID for each in-flight operation; duplicates reject with `operation_conflict`. Requests accept an AbortSignal and an absolute deadline. Cancelled or timed-out handlers keep their concurrency slot until they settle, so repeated timeouts cannot start unlimited background work. Session methods always report failures by rejecting.

Policy is local capability filtering, not server authorization. Enforce asset access and hosted-service entitlements at your own server boundary. Model credentials belong outside the browser.

[Add host adapters and extensions →](https://docs.lumencharts.com/charting-library/automation/extensions)

---
Documentation page: https://docs.lumencharts.com/charting-library/automation/commands-and-tools


---

<!-- Public documentation export; not library implementation source. -->
# Host adapters and extensions

## Custom commands

Declare a command with `defineCommand`. The `field` declarations produce both the JSON Schema your model sees and the parser the SDK runs, so the two cannot drift, and the handler's types are inferred from them.

```ts
import { defineCommand, field } from '@lumencharts/chart-automation/sdk';

const markEarnings = defineCommand({
  name: 'acme.markEarnings',
  effect: 'mutation',
  description: 'Mark the next earnings date with a vertical line.',
  instructions: 'Use only when the user asks about earnings.',
  examples: [{ description: 'Apple', input: { symbol: 'AAPL' } }],
  input: { symbol: field.string(16).describe('Ticker symbol, for example AAPL.') },
  output: { drawingId: field.string(64).describe('ID of the created line.') },
  async execute({ symbol }, context) {
    const time = await fetchNextEarnings(symbol, context.signal);
    const receipt = await context.execute('drawings.apply', {
      operations: [{ action: 'create', type: 'vertical_line', points: [{ time }] }],
    });
    return { drawingId: receipt.ids[0]! };
  },
});

const unregister = sdk.extensions.register(markEarnings);
await unregister.execute({ symbol: 'AAPL' });
```

| Field | Accepts |
| --- | --- |
| `field.string(max, { pattern })` / `field.text(max, { minLength, pattern })` | Non-blank / any string up to `max` Unicode code points. Supply an anchored (`^…$`) regular expression without flags. The parser evaluates its source in Unicode mode. |
| `field.integer({ minimum, maximum })` / `field.number(...)` | Safe integers / finite numbers; above `maximum` rejects with `limit_exceeded` |
| `field.boolean()`, `field.literal(value)`, `field.oneOf([...])` | Booleans, one exact value, one of a set |
| `field.array(item, { maxItems, minItems, uniqueItems })` | Bounded arrays with a required `maxItems`. `uniqueItems` rejects repeated JSON values regardless of object property order. |
| `field.object({...}, { minProperties })` | Nested closed objects; `minProperties` requires that many fields to be present |
| `field.union(discriminator, variants, { minProperties })` | Closed objects told apart by a string discriminator, advertised as `oneOf` with a `const` per variant; `minProperties` per variant counts the discriminator |
| `field.record(value, { maxKeys, maxKeyLength })` | Objects with your keys and one value shape; `maxKeys` is required, keys are 1 to `maxKeyLength` (default 64) characters |
| `field.scalar()` | A string, finite number or boolean |
| `field.unknown()` | Any JSON value, advertised without constraints; use only where your handler validates the value itself |
| `field.optional(inner)`, `field.nullable(inner)` | May be omitted / may be `null` |

Every field takes `.describe(text)` (1 to 1,000 characters), which becomes the field's `description` in the JSON Schema. Describe every field: the model sees nothing else about it. Descriptions pass through nested objects, arrays, unions and records, and `.describe()` keeps the field's inferred type.

Unions are keyed by discriminator value; each variant lists its other fields, and the handler receives a discriminated TypeScript union:

```ts
const annotate = defineCommand({
  name: 'acme.annotate',
  effect: 'mutation',
  description: 'Add price annotations.',
  input: {
    marks: field.array(field.union('kind', {
      level: { price: field.number().describe('Price of the level.') },
      zone: {
        from: field.number().describe('Lower price.'),
        to: field.number().describe('Upper price.'),
        label: field.optional(field.string(32)).describe('Short label.'),
      },
    }).describe('One annotation.'), { maxItems: 10 }).describe('Annotations to add.'),
  },
  output: { added: field.integer().describe('Annotations added.') },
  execute({ marks }) {
    for (const mark of marks) if (mark.kind === 'zone') console.log(mark.from, mark.to);
    return { added: marks.length };
  },
});
```

Unknown fields always reject. A rejected input carries `details` with the field path and a short fixed reason, for example `{ path: 'marks[2].to', reason: 'expected number' }`, and model tools return them as `{ error: 'invalid_input', details }` so the model can correct the call. Details never contain input values: paths name declared fields and array indexes only. Lower-level `AutomationCommand` definitions with a hand-written `{ jsonSchema, parse }` remain supported for shapes the builder cannot express; throw `new ChartCommandError(code, code, { path, reason })` to report details from your own parser.

| Command field | Contract |
| --- | --- |
| `name`, `version`, `description` | Namespaced stable identity (`acme.markEarnings`), positive version (default 1) and purpose |
| `effect` | `read`, `mutation` or `external`, checked against session policy |
| `input`, `output` | Field maps (or schemas); both boundaries are validated |
| `execute(input, context)` | Typed handler that obeys cancellation, deadline and target ownership |
| `instructions`, `examples` | Model guidance (up to 4,000 characters, joined by `sdk.instructions()`) and up to 8 examples, each checked against `input` at registration; `createAutomationTools` appends examples to the tool description as compact JSON lines, up to 2,000 characters |
| `concurrency` | `shared` or `exclusive`; defaults to `shared` for reads, `exclusive` otherwise |
| `toolName` | snake_case model name; defaults to the derived name |
| `targetChange` | `'synchronous'` only for a synchronous mutation that intentionally changes the chart target |
| `dispose()` | Optional cleanup, called once on unregister or session destruction |

The namespaces `data`, `capture`, `sdk`, `drawings`, `studies`, `settings`, `workspace` and `capabilities` are reserved. Native commands use a protected registrar, so an extension cannot replace a typed built-in operation.

### Composing commands

`context.execute(name, input)` runs another command, or an SDK port (`data.series`, `data.read`, `data.requestHistory`, `capture.image`), as part of the same request. Children share the signal, deadline and target fence, get a derived request ID and do not take another concurrency slot. A child's effect must be `read` or match the parent's, so a `read` command can never mutate or reach an external service. Target-changing commands cannot run as children, and nesting is limited to four levels. Calls to ports and built-in commands are typed: the result has the command's output type, and the input must match its input type or be typed `unknown` (the child validates it either way).

## Host ports

Use `createChartAutomation(host, policy)` when your application supplies its own data, capture and lifecycle integration. `openAutomation` and `createNativeChartAutomation` accept `additionalData`, `requestHistory` and `subscribeAdditionalData` for the standard chart.

| Port | Host responsibility |
| --- | --- |
| `currentTarget()` | Stable chart ID and generation that changes on selection or replacement, including A → B → A |
| `data.series()` | Stable unique series IDs, numeric field declarations, units and readiness |
| `data.read(input, context)` | Bounded local rows, revision and explicit loaded coverage |
| `data.requestHistory(input, context)` | Optional bounded provider ingestion, cancellation and coverage without navigating the chart |
| `capture.image(context)` | Local PNG and metadata belonging to the requested target |
| `subscribe(listener)` | Publish committed lifecycle/data changes; return a cleanup function |

The SDK validates declared fields and response shapes. Discovery permits up to 256 series with 128 fields per series; reads permit at most 10,000 rows and remain subject to the JSON byte budget. A descriptor uses this shape:

```ts
const series = {
  id: 'analytics.rsi',
  kind: 'study',
  timeUnit: 'milliseconds',
  readiness: 'ready',
  fields: [{ name: 'value', type: 'number', nullable: true }],
} as const;
// A warm-up row can be { time: 1735689600000, value: null }.
// A calculated row can be { time: 1735776000000, value: 52.4 }.
```

`time` is implicit and cannot be declared as a field. Undeclared fields, missing required values, non-finite numbers and unordered timestamps reject. History responses follow the same validation. Keep the provider's own work bounded before constructing results; a response-size check cannot make an unbounded database query safe.

## Durable saves

Native drawing and study changes apply locally with Undo. To save them before they appear, pass `persist`:

```ts
const sdk = await chart.openAutomation({
  async persist(change, context) {
    // change.family is 'drawings' or 'studies'; change.prepared.document is the candidate.
    await saveDocument(change.family, change.prepared.document, { operationId: context.requestId, signal: context.signal });
  },
});
```

`persist` runs after native preparation and before commit, while a mutation lease blocks manual edits. Reject to abort: the candidate is discarded and nothing is applied. If the request is cancelled after `persist` resolves, the change is saved but not applied; reconcile it as you would a lost response. Chart settings are never persisted.

## Side effects and lifecycle

The SDK rejects stale asynchronous results, but it cannot undo an external side effect or forcibly stop JavaScript. Check the AbortSignal and target before committing asynchronous work.

Completed-request idempotency belongs to your custom handler; the SDK only rejects duplicate in-flight request IDs. Native commands retain their native replay protection. This is an API for trusted extensions, not an untrusted-code sandbox.

Close sessions and release host subscriptions when the owning view is removed. Session cleanup attempts remaining command disposers even when one fails. Keep uploads, AI prompts and budgets, conversation storage and entitlement checks in host adapters so the same chart product works across applications.

## Testing commands

`createTestAutomation` gives you an in-memory chart for unit tests:

```ts
import { createTestAutomation } from '@lumencharts/chart-automation/sdk/testing';

const { sdk, select, setRows, remove } = createTestAutomation({
  rows: { main: [{ time: 1, open: 1, high: 2, low: 1, close: 2 }] },
});
sdk.extensions.register(myCommand);
expect(await sdk.execute('acme.lastClose', {})).toEqual({ close: 2 });

const earlier = sdk.request();
select();                                        // a symbol or interval change
await expect(sdk.execute('acme.lastClose', {}, earlier)).rejects.toMatchObject({ code: 'stale_binding' });
```

`setRows` replaces loaded rows and publishes `data`; `remove` simulates chart removal. Capture returns a 1×1 PNG unless `capture: false`.

### Native commands without a widget

Pass `owners` to install the native `drawings.*`, `studies.*`, `settings.*` and `workspace.*` commands, plus an optional `persist` hook, exactly as `installNativeAutomationCommands` does on a chart. `createFakeDrawingOwner()` is an in-memory drawing owner with native receipts, revisions, mutation leases and Undo, so a command that calls `context.execute('drawings.apply', …)` can be tested end to end:

```ts
import { createFakeDrawingOwner, createTestAutomation } from '@lumencharts/chart-automation/sdk/testing';

const drawings = createFakeDrawingOwner({ binding: { symbol: { ticker: 'AAPL' } } });
const saved: string[] = [];
const { sdk, select } = createTestAutomation({
  policy: { effects: ['read', 'mutation'] },
  owners: {
    drawings,
    async persist(change, context) {
      saved.push(context.requestId);            // runs before commit, under a lease
    },
  },
});
const command = sdk.extensions.register(markEarnings);

const { drawingId } = await command.execute({ symbol: 'AAPL' });
expect((await sdk.drawings.list({ offset: 0, limit: 10 })).drawings[0]?.id).toBe(drawingId);
expect(drawings.undo(saved[0]!)).toBe(true);   // native Undo by operation ID

const earlier = sdk.request();
select();
await expect(command.execute({ symbol: 'AAPL' }, earlier)).rejects.toMatchObject({ code: 'stale_binding' });
```

The fake supports `create`, `update` and `delete` operations on plain drawing summaries; group operations reject with `unsupported`. `leased()` reports whether a mutation lease is held, and `manualEdit(operationId, change)` simulates a hand-made edit that joins the same Undo history. Pass real owners from a chart instead to test against native behavior. `binding` defaults to the drawing owner's binding.

[Run the model on a server →](https://docs.lumencharts.com/charting-library/automation/remote)

---
Documentation page: https://docs.lumencharts.com/charting-library/automation/extensions


---

<!-- Public documentation export; not library implementation source. -->
# Server-side models

Most AI integrations call the model from a server while the chart runs in the browser. A remote session keeps the chart, its fences and its native owners in the browser and gives the server the same command facade and model tools.

```ts
// Browser
import { serveAutomation } from '@lumencharts/chart-automation/sdk';

const sdk = await chart.openAutomation({ policy: { effects: ['read', 'mutation'] } });
const server = serveAutomation(sdk, {
  captureResult: async capture => ({ attachmentId: await upload(capture.blob) }),
});
myChannel.onCall(async (call, signal) => myChannel.reply(await server.handle(call, signal)));
startTurn({ chart: server.snapshot() });
```

```ts
// Server
import { createAutomationTools, connectAutomation } from '@lumencharts/chart-automation/sdk';

const chart = connectAutomation(turn.chart, {
  send: (call, signal) => myChannel.call(call, signal),
});
const tools = createAutomationTools(chart, { use: [budgets, auditLog], imageResult: toProviderImage });
const system = chart.instructions();
```

## Protocol

| Message | Shape |
| --- | --- |
| Snapshot | `{ version: 1, target, capabilities, instructions }`, sent by the browser at the start of a turn and again when `capabilities().revision` changes (`chart.update(snapshot)`) |
| Call | `{ version: 1, requestId, target, name, commandVersion?, input, timeoutMs? }` |
| Result | `{ version: 1, ok: true, value, target }` or `{ version: 1, ok: false, code, target }` |

`name` is a command name or one of `data.series`, `data.read`, `data.requestHistory` and `capture.image`. Timeouts are relative, so server and browser clocks need not agree; the remote session also enforces the snapshot's deadline ceiling locally. Results carry the chart's current target, so the server learns about a navigation and later calls are fenced to the new chart. A call built against a different `commandVersion` rejects with `unsupported`. Errors carry a chart error code and, for rejected input, optional `details` (`{ path, reason }`), never a message. `parseAutomationRemoteResult` validates and bounds `details` and drops anything else.

Capture is advertised only when the browser supplies `captureResult`, which turns the PNG into something your channel can carry; on the server, `capture.image()` and the `capture_image` tool resolve to that value.

## Transport responsibilities

The SDK is transport-neutral. Your channel owns:

- delivery and reconnection;
- durable replay by `requestId`: a repeated call returns the stored result instead of running twice;
- rejecting a known `requestId` sent with different input;
- forwarding cancellation through the `signal` on both sides.

`server.handle` never rejects; it always resolves to a result. `chart.destroy()` aborts in-flight calls on the server side, and later calls reject with `unavailable`.

---
Documentation page: https://docs.lumencharts.com/charting-library/automation/remote
