Skip to content

Prompt Serialization

toPromptContext(), toHistoryContext(), and toContext() accept an AskablePromptContextOptions object to control exactly how context is serialized. Use toPromptContextAsync() or toContextAsync() when you also want to include registered app-owned context sources.

Presets

Named presets are shorthand for common option combinations. Specify a preset via the preset option — any individual options you pass alongside it override the preset.

PresetEquivalent optionsUse case
compact{ includeText: false, format: 'natural' }Tight token budgets, meta-only
verbose{ includeText: true, format: 'natural' }Full natural language (same as default)
json{ format: 'json', includeText: true }Structured output for tool calls / storage
ts
ctx.toPromptContext({ preset: 'compact' });
// → "User is focused on: — metric: revenue, delta: -12%"

ctx.toPromptContext({ preset: 'json' });
// → '{"meta":{"metric":"revenue","delta":"-12%"},"text":"Revenue","timestamp":...}'

// Individual options override the preset
ctx.toPromptContext({ preset: 'compact', includeText: true });
// compact but with text included

Presets work with all serialization methods: toPromptContext(), toHistoryContext(), toContext(), and serializeFocus().

Default output

ts
ctx.toPromptContext();
// → "User is focused on: — metric: revenue, delta: -12%, period: Q3 — value "Revenue Chart""

When nothing is focused:

ts
ctx.toPromptContext();
// → "No UI element is currently focused."

Format

Natural language (default)

ts
ctx.toPromptContext({ format: 'natural' });
// → "User is focused on: — metric: revenue, delta: -12% — value "Revenue""

JSON

ts
ctx.toPromptContext({ format: 'json' });
// → '{"meta":{"metric":"revenue","delta":"-12%"},"text":"Revenue","timestamp":1712345678}'

// No focus → 'null'
ctx.toPromptContext({ format: 'json' });

Controlling meta output

Exclude keys

Strip sensitive or irrelevant keys before serialization:

ts
ctx.toPromptContext({ excludeKeys: ['_id', 'debug', 'internalRef'] });

Key order

Promote the most important keys to the front of the output:

ts
ctx.toPromptContext({ keyOrder: ['metric', 'value', 'period'] });
// → "User is focused on: — metric: revenue, value: $128k, period: Q3, ..."

Keys not listed in keyOrder appear after, in their original order.

Controlling text

Omit text

ts
ctx.toPromptContext({ includeText: false });
// → "User is focused on: — metric: revenue, delta: -12%"

Truncate text

ts
ctx.toPromptContext({ maxTextLength: 100 });
// text content is sliced to 100 characters

Token budget

maxTokens truncates the entire output to an approximate token count using a 4 chars/token estimate. No external tokenizer required.

ts
ctx.toPromptContext({ maxTokens: 50 });
// If the output exceeds ~200 chars:
// → "User is focused on: — metric: revenue, delta: -12%, period:... [truncated]"

This is especially useful for toHistoryContext() where multiple entries can accumulate:

ts
ctx.toHistoryContext(10, { maxTokens: 300 });
// History trimmed to fit ~1200 chars, with [truncated] marker if needed

App-owned sources

DOM context only includes what is rendered. For paginated tables, virtualized lists, documents, maps, charts, calendars, canvases, or API-backed dashboards, register a source that resolves data from your application state.

ts
import {
  createAskableCollectionSource,
  createAskablePageSource,
  createAskableSource,
} from '@askable-ui/core';

ctx.registerSource('accounts', createAskableCollectionSource({
  describe: 'Customer accounts matching the active filters',
  getState: () => ({ filters, sort, page, pageSize, totalCount }),
  getVisibleItems: () => table.getVisibleRows(),
  getSelectedItems: ({ selection }) => getAccountsByIds(selection),
  getItems: () => accountStore.getAllMatching({ filters, sort }),
  getSummary: ({ maxItems }) => summarizeAccounts({ filters, sort, maxItems }),
  sanitizeItem: redactAccountFields,
  sanitize: (source) => ({
    ...source,
    state: redactFilterState(source.state),
  }),
}));

ctx.registerSource('active-chart', createAskableSource({
  kind: 'chart',
  describe: 'Revenue chart',
  state: () => ({ range, segment }),
  modes: {
    summary: () => chart.summary(),
    selected: ({ selection }) => chart.pointsForSelection(selection),
    all: ({ maxItems }) => chart.series().slice(0, maxItems),
  },
}));

ctx.registerSource('page', createAskablePageSource({
  includeLinks: true,
  sanitizeText: redactPageText,
}));

const context = await ctx.toPromptContextAsync({
  sources: [{ id: 'accounts', mode: 'all', maxItems: 20, timeoutMs: 750 }],
  sourceErrorMode: 'include',
});

This keeps Askable generic: interaction tools capture what the user meant, and source resolvers supply what the app knows. createAskableCollectionSource() is useful when the app owns more items than the DOM renders; mode: 'all' asks that source for the logical collection, while maxItems keeps prompt budgets explicit. createAskableSource() covers non-collection state like documents, charts, maps, canvases, or workflows; use modes to expose named slices such as summary, selected, all, or app-defined modes without writing a custom resolver switch. Source output can be redacted with sanitizeItem, a source-level sanitize hook, or a context-level sanitizeSource hook. Failed or timed-out sources are represented with a safe unavailable marker by default; use sourceErrorMode: 'omit' or 'throw' when your runtime needs stricter behavior.

For browser extensions and other clients that cannot change the site code, createAskablePageSource() can still expose the page title, URL, selected text, headings, optional links, and capped full-page text as a normal source.

Custom labels

ts
ctx.toPromptContext({
  prefix: 'Active element:',
  textLabel: 'label',
});
// → "Active element: — metric: revenue — label "Revenue Chart""

serializeFocus() — structured data

When you need the data before formatting it as a string:

ts
const data = ctx.serializeFocus();
// → { meta: { metric: 'revenue', delta: '-12%' }, text: 'Revenue', timestamp: 1712345678 }
// → null if nothing focused

// Same options apply
const data = ctx.serializeFocus({ includeText: false, excludeKeys: ['debug'] });

Use this to:

  • Store focus events in a database
  • Build a custom serialization format
  • Pass structured data to a tool call / function call instead of a string

All options at a glance

OptionTypeDefaultDescription
preset'compact' | 'verbose' | 'json'Named shorthand; individual options override it
format'natural' | 'json''natural'Output format
includeTextbooleantrueInclude element text content
maxTextLengthnumberTruncate text to N characters
excludeKeysstring[]Omit these keys from object meta
keyOrderstring[]Promote these keys to the front
prefixstring'User is focused on:'Prefix in natural format
textLabelstring'value'Label for text in natural format
maxTokensnumberToken budget (4 chars/token estimate). Appends [truncated] if exceeded.

toContext() — combined output

Instead of manually concatenating toPromptContext() and toHistoryContext(), use toContext() to get both in a single call:

ts
// Replaces this:
const systemPrompt = [
  'Current selection:',
  ctx.toPromptContext(),
  '',
  'Recent interactions:',
  ctx.toHistoryContext(5),
].join('\n');

// With this:
const systemPrompt = ctx.toContext({ history: 5 });

All prompt options pass through:

ts
ctx.toContext({ history: 3, preset: 'compact', maxTokens: 300 });
ctx.toContext({ history: 5, currentLabel: 'Active', historyLabel: 'Previous' });

Released under the MIT License.