Skip to Content

@stesura/ai

Headless core of the AI feature: the autocomplete plugin, the text-action engine, the document-chat tool system and the HTTP transport. Framework-free; the UI is in @stesura/ai-react and the route handlers in @stesura/ai-server. Setup and behaviour are in the AI guide.

Transport

createHttpGenerateText turns a route URL into the GenerateText function the text actions and autocomplete call.

createHttpGenerateText

GenerateText transport that POSTs to an @stesura/ai-server route ({ actionId, target, before, after, params } in, { text } out). The prompt itself lives on the server — only data fragments travel.

const generate = createHttpGenerateText( `${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/ai/text-action` );

Throws AiTransportError on a non-2xx response. A hung request is aborted after timeoutMs (default 60s; 0 disables) — a fetch that never settles would otherwise pin a spinner for as long as the browser tolerates it.

Parameters:
NameTypeDefault
urlstring
initOmit<RequestInit, "method" | "body" | "signal"> & { timeoutMs?: number; }
Returns:
GenerateText

GenerateText

Transport for text generation. The package is backend-agnostic (same pattern as proofread’s generateProofreadErrors): the host app decides whether this hits a Next.js route, the Vercel AI SDK, or a test fake.

Parameters:
NameTypeDefault
requestAiActionRequest
Returns:
Promise<string>

AiActionRequest

What a generation request carries over the wire: which server-owned prompt template to run, plus the data fragments it interpolates. Deliberately no system/prompt — the client never authors prompt text (AIS-2).

NameTypeDefault
actionIdstring

Built-in AiActionId, or a custom id the host registered on the server’s actions option. Typed as string so hosts can add their own.

targetstring

The text the action operates on.

beforestring

Read-only context around the target.

afterstring
paramsTextActionParams
signalAbortSignal

AiTransportError

A non-2xx response from an @stesura/ai-server route. Carries the upstream status and retry-after so callers can back off on a 429/529 instead of treating an overload like a permanent failure.

NameTypeDefault
statusnumber
retryAfternumber | undefined

Seconds to wait before retrying, when the server said.

namestring
messagestring
stackstring
causeunknown

Autocomplete

Ghost-text completions. autocompletePluginKey, acceptAiSuggestion and clearAiSuggestion are for hosts that bind their own keys or render their own accept/dismiss controls.

autocompletePlugin

Ghost-text tab autocomplete.

Debounced on typing; only triggers with the cursor at the end of a non-empty textblock, and only for local edits that touch that block — a collaborator typing elsewhere is not a reason to spend a request. The suggestion renders as a widget decoration (never enters the doc) and survives changes that don’t reach its block (a collaborator elsewhere). Tab accepts, Escape or any non-matching edit discards, and typing the suggestion’s own next characters trims it instead of discarding. Responses are cached per (block id, context, prefix) and in-flight requests are aborted on every new trigger; an overloaded backend backs the plugin off instead of poisoning the cache.

Uses handleDOMEvents.keydown (not handleKeyDown) so Tab wins over the core keymap’s indent behaviour regardless of plugin order.

v0 limitation: the ghost text is not measured by pagination (shadow DOM measuring ignores decorations), so a suggestion on the last line of a page can visually overflow until accepted or discarded.

Parameters:
NameTypeDefault
optionsAutocompletePluginOptions
Returns:
Plugin<any>

AutocompletePluginOptions

Options for autocompletePlugin. Only generate is required.

NameTypeDefault
generateGenerateText

Completion transport — point this at a cheap/fast model route.

debounceMsnumber

Idle time after a doc change before requesting a completion.

contextBlocksnumber

Previous sibling blocks included as context. 0 sends the current block only.

minPrefixCharsnumber

Minimum characters in the current block before triggering.

isEnabled() => boolean

Host hook to toggle the feature (e.g. a settings store).

maxCacheEntriesnumber

Completion cache entries kept per editor (keyed by block id + prefix).

onGhostTextChange(view: EditorView, nodeId: string) => void

Called when a block’s ghost text appears, changes or disappears — i.e. its rendered height may have changed without a doc change. Hosts with pagination should wire this to requestNodeRemeasure from @stesura/pagination so ghost text repaginates correctly.

autocompletePluginKey

Key for reading the autocomplete state (e.g. to gate keymaps on a visible ghost).

NameTypeDefault
get(state: EditorState) => Plugin<AutocompletePluginState> | undefined

Get the active plugin with this key, if any, from an editor state.

getState(state: EditorState) => AutocompletePluginState | undefined

Get the plugin’s state from an editor state.

AutocompletePluginState

Plugin state: the ghost suggestion currently rendered, if any.

NameTypeDefault
suggestionGhostSuggestion | null

GhostSuggestion

A completion rendered as ghost text after the cursor.

NameTypeDefault
posnumber

Cursor position the suggestion is anchored to.

textstring
nodeIdstring

uniqueId of the block the ghost text renders in (“anonymous” when unset).

acceptAiSuggestion

Accept the current ghost suggestion: insert it as regular typed content (the track-changes transaction modifier treats it like user input).

Parameters:
NameTypeDefault
viewEditorView
Returns:
boolean

clearAiSuggestion

Discard the current ghost suggestion (Escape behavior). Returns false when none is shown.

Parameters:
NameTypeDefault
viewEditorView
Returns:
boolean

Text actions

runTextAction is what the text-menu dropdown calls; use it directly for custom triggers.

runTextAction

Run a text-menu AI action on the current selection: serialize the selected blocks → generate → parse → apply as a minimal edit, transformed into track-changes suggestions (even when track changes is off) so the user reviews with the standard accept/reject UI. One transaction, one undo step.

When the document changes during generation, the target is re-found by block id and the answer still lands if the target itself is untouched; otherwise (or without the uniqueId plugin) the result is stale.

Never throws: transport failures and aborts come back as { status: "failed" } so callers branch on the result instead of pairing every call with a try/catch.

Parameters:
NameTypeDefault
optionsRunTextActionOptions
Returns:
Promise<TextActionResult>

RunTextActionOptions

Options for runTextAction.

NameTypeDefault
viewEditorView
actionTextActionId
paramsTextActionParams
generateGenerateText

Backend transport — see GenerateText.

userCurrentUser

Suggestion author; defaults to a dedicated AI identity.

requestedBy{ id: string; name?: string; }

The signed-in user who ran the action, kept on the suggestion.

contextBlocksnumber

Sibling blocks sent as read-only context around the target.

signalAbortSignal

TextActionId

"rephrase" | "changeStyle" | "translate" | "simplify" | "continueWriting" — the built-in actions runTextAction understands.

TextActionParams

Per-action parameters; each field is length-capped on the wire.

NameTypeDefault
tonestring

Target tone for changeStyle (e.g. “professional”, “casual”).

languagestring

Target language for translate (e.g. “French”, “de-DE”).

instructionsstring

Free-form user instructions appended to any action.

TextActionResult

What runTextAction resolves with. It never throws: failures come back as a result.

statusFieldsMeaning
"applied"warnings: Warning[]Suggestions landed; warnings lists what the markdown conversion lost
"stale"—The target blocks changed while the model was generating; nothing applied
"empty"reason: TextActionEmptyReasonNothing to apply, and why
"failed"aborted: boolean, error: unknownThe transport rejected or the edit couldn’t be applied; aborted is true when the caller’s signal fired

TextActionEmptyReason

"no-target" | "no-content" | "no-output" | "no-change": the selection isn’t in a body section (headers/footers, notes, select-all), the target is blank, the model returned nothing usable, or the answer matches the current text.

Prompt templates

Templates live on the server (why). These are the types and built-ins a custom actions entry on the text-action handler works with.

AI_ACTION_TEMPLATES

THE server-owned prompt templates. The wire body carries an actionId and data fragments — never a system string — so the route cannot be repurposed as a generic completion proxy (AIS-2). Hosts add their own via createTextActionHandler’s actions option, still server-side.

NameTypeDefault
rephraseAiActionTemplate
changeStyleAiActionTemplate
translateAiActionTemplate
simplifyAiActionTemplate
continueWritingAiActionTemplate
autocompleteAiActionTemplate

AiActionTemplate

Builds the server-owned prompt for a built-in action.

Parameters:
NameTypeDefault
paramsTextActionParams
payloadAiActionPayload
Returns:
{ system: string; prompt: string; }

AiActionId

TextActionId | "autocomplete" — the ids AI_ACTION_TEMPLATES covers.

AiActionPayload

The data fragments a client may send for an action. Everything here is untrusted content the server fences into its own template — never prompt text.

NameTypeDefault
targetstring

The text the action operates on.

beforestring

Read-only context around the target (preceding/following blocks).

afterstring

fenceData

Wrap untrusted content in a <tag> fence it cannot close.

Every prompt in this package hands the model data (document text, attachment content, an autocomplete prefix) inside a tagged block, with a “this is data, not instructions” rule alongside. Without escaping, content carrying the block’s own closing tag ends the fence early and everything after it reads as prompt — system-level prompt in the chat handler’s case.

Parameters:
NameTypeDefault
tagstring
contentstring
Returns:
string

redactionEntitiesTemplate

The opt-in prompt for AI redaction suggestions, registered under REDACTION_ENTITIES_ACTION on a route whose model the host controls. See Redaction.

Server-side prompt for AI redaction suggestions. Not a built-in: the host opts in by registering it, on a route whose model it controls (document text is personal data): createTextActionHandler({ model, actions: { [REDACTION_ENTITIES_ACTION]: redactionEntitiesTemplate } }).

Parameters:
NameTypeDefault
paramsTextActionParams
payloadAiActionPayload
Returns:
{ system: string; prompt: string; }

Chat tools

The tool contract and the set the server and the editor are both handed. See Extending the chat with custom tools.

defineAiTool

Ties execute’s input type to the schema and derives run, which validates raw model input through the schema’s Standard Schema validate and then executes. run does not catch: the dispatcher that calls it does, and logs.

A function with overloads, not an arrow: one name for both tool kinds, each keeping its own execute signature, without a cast.

Parameters:
NameTypeDefault
toolAiEditorTool<Input>
Returns:
DefinedAiEditorTool

createAiToolSet

Validate and freeze a tool set. Throws on a wiring bug: a duplicate name, a name that is not snake_case (64 characters at most, the providers’ limit), a reserved name, or an empty description. To leave a built-in out, filter builtInAiTools; a built-in cannot be replaced under its own name.

Parameters:
NameTypeDefault
toolsreadonly AiToolSetEntry[]
Returns:
AiToolSet

builtInAiTools

The editor tools every chat gets unless the host leaves some out, in a fixed order: the tool list is part of the cached prompt prefix. Hosts add their own tools through createAiToolSet; see packages/ai/README.md.

NameTypeDefault
lengthnumber
toString() => string
toLocaleString{ (): string; (locales: string | string[], options?: (NumberFormatOptions & DateTimeFormatOptions) | undefined): string; }
concat{ (...items: ConcatArray<DefinedAiEditorTool>[]): DefinedAiEditorTool[]; (...items: (DefinedAiEditorTool | ConcatArray<...>)[]): DefinedAiEditorTool[]; }
join(separator?: string | undefined) => string
slice(start?: number | undefined, end?: number | undefined) => DefinedAiEditorTool[]
indexOf(searchElement: DefinedAiEditorTool, fromIndex?: number | undefined) => number
lastIndexOf(searchElement: DefinedAiEditorTool, fromIndex?: number | undefined) => number
every{ <S extends DefinedAiEditorTool>(predicate: (value: DefinedAiEditorTool, index: number, array: readonly DefinedAiEditorTool[]) => value is S, thisArg?: any): this is readonly S[]; (predicate: (value: DefinedAiEditorTool, index: number, array: readonly DefinedAiEditorTool[]) => unknown, thisArg?: any): boolean; }
some(predicate: (value: DefinedAiEditorTool, index: number, array: readonly DefinedAiEditorTool[]) => unknown, thisArg?: any) => boolean
forEach(callbackfn: (value: DefinedAiEditorTool, index: number, array: readonly DefinedAiEditorTool[]) => void, thisArg?: any) => void
map<U>(callbackfn: (value: DefinedAiEditorTool, index: number, array: readonly DefinedAiEditorTool[]) => U, thisArg?: any) => U[]
filter{ <S extends DefinedAiEditorTool>(predicate: (value: DefinedAiEditorTool, index: number, array: readonly DefinedAiEditorTool[]) => value is S, thisArg?: any): S[]; (predicate: (value: DefinedAiEditorTool, index: number, array: readonly DefinedAiEditorTool[]) => unknown, thisArg?: any): DefinedAiEditorTool[]; }
reduce{ (callbackfn: (previousValue: DefinedAiEditorTool, currentValue: DefinedAiEditorTool, currentIndex: number, array: readonly DefinedAiEditorTool[]) => DefinedAiEditorTool): DefinedAiEditorTool; (callbackfn: (previousValue: DefinedAiEditorTool, currentValue: DefinedAiEditorTool, currentIndex: number, array: readonly ...
reduceRight{ (callbackfn: (previousValue: DefinedAiEditorTool, currentValue: DefinedAiEditorTool, currentIndex: number, array: readonly DefinedAiEditorTool[]) => DefinedAiEditorTool): DefinedAiEditorTool; (callbackfn: (previousValue: DefinedAiEditorTool, currentValue: DefinedAiEditorTool, currentIndex: number, array: readonly ...
find{ <S extends DefinedAiEditorTool>(predicate: (value: DefinedAiEditorTool, index: number, obj: readonly DefinedAiEditorTool[]) => value is S, thisArg?: any): S | undefined; (predicate: (value: DefinedAiEditorTool, index: number, obj: readonly DefinedAiEditorTool[]) => unknown, thisArg?: any): DefinedAiEditorTool | un...
findIndex(predicate: (value: DefinedAiEditorTool, index: number, obj: readonly DefinedAiEditorTool[]) => unknown, thisArg?: any) => number
entries() => ArrayIterator<[number, DefinedAiEditorTool]>
keys() => ArrayIterator<number>
values() => ArrayIterator<DefinedAiEditorTool>
includes(searchElement: DefinedAiEditorTool, fromIndex?: number | undefined) => boolean
flatMap<U, This = undefined>(callback: (this: This, value: DefinedAiEditorTool, index: number, array: DefinedAiEditorTool[]) => U | readonly U[], thisArg?: This | undefined) => U[]
flat<A, D extends number = 1>(this: A, depth?: D | undefined) => FlatArray<A, D>[]
at(index: number) => DefinedAiEditorTool | undefined
__@iterator@6565() => ArrayIterator<DefinedAiEditorTool>
__@unscopables@6567{ readonly [x: number]: boolean | undefined; readonly length?: boolean; toString?: boolean; toLocaleString?: boolean; concat?: boolean; join?: boolean; slice?: boolean; indexOf?: boolean; lastIndexOf?: boolean; ... 17 more ...; readonly [Symbol.unscopables]?: boolean; }

AiToolDeclaration

What the model sees of a tool, and what the server needs to declare it. Static: the server route and the editor both import it. The executor is bound where its dependencies live (see AiEditorTool, AiServerTool).

NameTypeDefault
namestring

snake_case, unique in the tool set.

descriptionstring

What the tool does and when to use it. The model reads it, and tool search matches against it: a tool search cannot find is a tool the agent never uses. A static string: interpolated dates or counts break prompt caching.

inputSchemaAiToolSchema<Input>
kindAiToolKind
runsOn"editor" | "server"

editor tools run in the browser against the document; server tools in the host’s route.

deferboolean

Deferred tools are loaded on demand through tool search. Default true; the few tools nearly every turn needs set false.

labelsAiToolLabels

AiEditorTool

A tool that runs in the browser, against the document.

Executor contract:

  • Always return a string: results, warnings and errors alike. The model reads it and self-corrects; nothing may throw for bad input.
  • Content edits land as track-changes suggestions (dispatchAsSuggestion); never auto-accept.
  • Resolve block ids at execution time: the document may have changed since the model read it, and a vanished id is a polite error, not a crash.
NameTypeDefault
namestring

snake_case, unique in the tool set.

descriptionstring

What the tool does and when to use it. The model reads it, and tool search matches against it: a tool search cannot find is a tool the agent never uses. A static string: interpolated dates or counts break prompt caching.

inputSchemaAiToolSchema<Input>
kindAiToolKind
runsOn"editor"

editor tools run in the browser against the document; server tools in the host’s route.

deferboolean

Deferred tools are loaded on demand through tool search. Default true; the few tools nearly every turn needs set false.

labelsAiToolLabels
execute(view: EditorView, input: Input, options: AiToolExecutionOptions) => string | Promise<string>

AiServerTool

A tool that runs in the host’s API route, next to its secrets, and never sees the editor. Same contract as editor tools: always a string, never throw. Authorise as context.user, never as the operator alone. A tool with side effects outside the editor declares kind: "config", so the user confirms it.

NameTypeDefault
namestring

snake_case, unique in the tool set.

descriptionstring

What the tool does and when to use it. The model reads it, and tool search matches against it: a tool search cannot find is a tool the agent never uses. A static string: interpolated dates or counts break prompt caching.

inputSchemaAiToolSchema<Input>
kind"read" | "config"
runsOn"server"

editor tools run in the browser against the document; server tools in the host’s route.

deferboolean

Deferred tools are loaded on demand through tool search. Default true; the few tools nearly every turn needs set false.

labelsAiToolLabels
execute(input: Input, context: AiServerToolContext) => string | Promise<string>

AiToolExecutionOptions

Options an editor tool’s execute receives.

NameTypeDefault
userCurrentUser

Suggestion author for edits made by tools.

requestedBy{ id: string; name?: string; }

The signed-in user the agent works for, stamped on its tracked edits as requestedById / requestedByName, so one user’s accept all never resolves another user’s agent edits.

onSuggestion(trackChangesId: string) => void

Called after a tool lands an edit as tracked suggestions, with the trackChangesId every mark/attr of that edit carries. Hosts use it to offer per-tool-call accept/reject.

toolCallIdstring

The model’s id for this call.

sessionAiToolSession

What the agent has read this conversation; without one, edits merge against the current text.

createAiToolSession

The read ledger of one conversation: the last version of each block read_document showed in full, and the view. Editing tools merge an edit against it. Keep one per conversation and start a new one when the conversation is cleared or restored: a ledger that outlives what the model remembers would let it edit blocks it no longer has in context.

Parameters:
This function does not accept any parameters.
Returns:
AiToolSession

AiToolSession

What the agent has read in one conversation, so an edit merges against the text it saw rather than the block as it is now: a user’s change since the read survives the agent’s rewrite (docs/executed/ai-editing-plan.md, section 2). Hosts keep one per conversation, a fresh one when it is cleared or restored.

NameTypeDefault
recordRead(blocks: readonly AiReadBlock[], view: AiReadView) => void

read_document showed these blocks in full, as they are now, in this view.

recordWritten(blocks: readonly Node[], changeId: string) => void

The agent wrote these blocks (each carrying the id of the block it rewrote) in the call that made suggestion changeId: what it now takes them to say, whatever the merge kept of others’.

shown(blockId: string) => { node: Node; shown: Node; original: Node; view: AiReadView; changeIds: readonly string[]; } | undefined

What the agent last read or wrote of a block: the block then, the view, the suggestions it wrote there since it last read it, and what it read before those (original).

AiReadView

"final" | "markup" | "original". How read_document showed tracked changes: as accepted, inline, or as rejected.

AiToolKind

"read" | "annotate" | "edit" | "config". Drives the ask-mode gate and the chat UI: read tools stay available in ask mode and render status text; annotate tools (comments) stay available in ask mode and render the review chip; edit tools render the accept/reject chip; config tools apply untracked configuration, or a server tool’s side effect, and are confirmed before running.

AiChatMode

"ask" | "edit" (default "edit"). In ask mode the model only gets read and annotate tools.

Compare tools

compare_documents and compare_details compare the open document with an attached .docx or a saved version. The route declares compareToolDeclarations; the editor page binds the executors with createCompareTools, giving it the chat’s attachments and, optionally, a versions source.

createCompareTools

The compare tools, bound to where the documents come from: the chat’s attachments and, optionally, saved versions. Without getVersion, attachments only. Bind them where the sources live (the editor page), and declare compareToolDeclarations on the route.

Parameters:
NameTypeDefault
optionsCreateCompareToolsOptions
Returns:
DefinedAiEditorTool[]

CreateCompareToolsOptions

NameTypeDefault
attachments() => readonly CompareAttachment[]

The chat’s imported .docx attachments, read at each call (ai-react’s chat store keeps them).

getVersion(id: string) => JSONContent | Promise<JSONContent | null | undefined> | null | undefined

A saved version’s document JSON; null or undefined when there is none.

listVersions() => readonly CompareVersionInfo[] | Promise<readonly CompareVersionInfo[]>

The saved versions offered to the agent, newest first.

CompareAttachment

An attached document, imported: what compare_documents compares with attachment:<name>.

NameTypeDefault
namestring
docNode

CompareVersionInfo

A saved version listVersions offers.

NameTypeDefault
idstring
labelstring
datestring

compareToolDeclarations

The compare tools’ declarations, for the chat route. Same order as createCompareTools.

NameTypeDefault
lengthnumber
toString() => string
toLocaleString{ (): string; (locales: string | string[], options?: (NumberFormatOptions & DateTimeFormatOptions) | undefined): string; }
concat{ (...items: ConcatArray<AiToolDeclaration>[]): AiToolDeclaration[]; (...items: (AiToolDeclaration | ConcatArray<AiToolDeclaration>)[]): AiToolDeclaration[]; }
join(separator?: string | undefined) => string
slice(start?: number | undefined, end?: number | undefined) => AiToolDeclaration[]
indexOf(searchElement: AiToolDeclaration, fromIndex?: number | undefined) => number
lastIndexOf(searchElement: AiToolDeclaration, fromIndex?: number | undefined) => number
every{ <S extends AiToolDeclaration>(predicate: (value: AiToolDeclaration, index: number, array: readonly AiToolDeclaration[]) => value is S, thisArg?: any): this is readonly S[]; (predicate: (value: AiToolDeclaration, index: number, array: readonly AiToolDeclaration[]) => unknown, thisArg?: any): boolean; }
some(predicate: (value: AiToolDeclaration, index: number, array: readonly AiToolDeclaration[]) => unknown, thisArg?: any) => boolean
forEach(callbackfn: (value: AiToolDeclaration, index: number, array: readonly AiToolDeclaration[]) => void, thisArg?: any) => void
map<U>(callbackfn: (value: AiToolDeclaration, index: number, array: readonly AiToolDeclaration[]) => U, thisArg?: any) => U[]
filter{ <S extends AiToolDeclaration>(predicate: (value: AiToolDeclaration, index: number, array: readonly AiToolDeclaration[]) => value is S, thisArg?: any): S[]; (predicate: (value: AiToolDeclaration, index: number, array: readonly AiToolDeclaration[]) => unknown, thisArg?: any): AiToolDeclaration[]; }
reduce{ (callbackfn: (previousValue: AiToolDeclaration, currentValue: AiToolDeclaration, currentIndex: number, array: readonly AiToolDeclaration[]) => AiToolDeclaration): AiToolDeclaration; (callbackfn: (previousValue: AiToolDeclaration, currentValue: AiToolDeclaration, currentIndex: number, array: readonly AiToolDeclarat...
reduceRight{ (callbackfn: (previousValue: AiToolDeclaration, currentValue: AiToolDeclaration, currentIndex: number, array: readonly AiToolDeclaration[]) => AiToolDeclaration): AiToolDeclaration; (callbackfn: (previousValue: AiToolDeclaration, currentValue: AiToolDeclaration, currentIndex: number, array: readonly AiToolDeclarat...
find{ <S extends AiToolDeclaration>(predicate: (value: AiToolDeclaration, index: number, obj: readonly AiToolDeclaration[]) => value is S, thisArg?: any): S | undefined; (predicate: (value: AiToolDeclaration, index: number, obj: readonly AiToolDeclaration[]) => unknown, thisArg?: any): AiToolDeclaration | undefined; }
findIndex(predicate: (value: AiToolDeclaration, index: number, obj: readonly AiToolDeclaration[]) => unknown, thisArg?: any) => number
entries() => ArrayIterator<[number, AiToolDeclaration]>
keys() => ArrayIterator<number>
values() => ArrayIterator<AiToolDeclaration>
includes(searchElement: AiToolDeclaration, fromIndex?: number | undefined) => boolean
flatMap<U, This = undefined>(callback: (this: This, value: AiToolDeclaration, index: number, array: AiToolDeclaration[]) => U | readonly U[], thisArg?: This | undefined) => U[]
flat<A, D extends number = 1>(this: A, depth?: D | undefined) => FlatArray<A, D>[]
at(index: number) => AiToolDeclaration | undefined
__@iterator@6565() => ArrayIterator<AiToolDeclaration>
__@unscopables@6567{ readonly [x: number]: boolean | undefined; readonly length?: boolean; toString?: boolean; toLocaleString?: boolean; concat?: boolean; join?: boolean; slice?: boolean; indexOf?: boolean; lastIndexOf?: boolean; ... 17 more ...; readonly [Symbol.unscopables]?: boolean; }

compareDocumentsDeclaration

NameTypeDefault
namestring
runsOn"editor"
kind"read"
deferfalse
labels{ running: string; complete: string; }
descriptionstring
inputSchemaZodObject<{ with: ZodOptional<ZodString>; }, $strip>

compareDetailsDeclaration

NameTypeDefault
namestring
runsOn"editor"
kind"read"
deferfalse
labels{ running: string; complete: string; }
descriptionstring
inputSchemaZodObject<{ with: ZodOptional<ZodString>; clause: ZodOptional<ZodString>; fromIndex: ZodOptional<ZodNumber>; }, $strip>

Issue tools

list_issues, fix_issues and apply_format_fixes give the agent the editor’s checkers: proofread’s spelling and grammar errors (fixed as tracked suggestions), format-lint’s suggestions (applied directly after the user confirms) and broken cross-references. The route declares issueToolDeclarations; the editor page binds the executors with createIssueTools, passing the providers proofreadIssues, formatLintIssues and referenceIssues from their packages.

createIssueTools

The issue tools over the given sources (proofreadIssues from @stesura/proofread, formatLintIssues from @stesura/format-lint, referenceIssues from @stesura/references, or the host’s own). Bind them on the editor page and declare issueToolDeclarations on the route.

Parameters:
NameTypeDefault
providersreadonly IssueProvider[]
Returns:
DefinedAiEditorTool[]

IssueProvider

One source of findings for the agent. Lives in the package that owns the findings and is passed to createIssueTools in @stesura/ai. A provider whose plugin is not in the editor collects nothing.

NameTypeDefault
sourcestring
collect(state: EditorState) => AiIssue[]

The current findings, in document order.

apply(state: EditorState, fixes: readonly AiIssueFix[]) => AiIssueApplyResult

Applies fixes to issues collect returns. Tracked fixes come back as a plain edit the caller turns into suggestions; direct ones as the change itself. Omitted: nothing is fixable.

AiIssue

A finding a source reports to the agent’s list_issues.

NameTypeDefault
idstring

Stable across runs while the finding stands, unique within its source.

sourcestring

"proofread", "format-lint", "references", …

rulestring

The source’s own rule id: "spelling", "FMT-11", "reference-missing".

severity"error" | "warning" | "hint" | "info"
messagestring
range{ from: number; to: number; }

Document range of the finding, when it has one: the agent sees the text there.

blockIdsstring[]

Blocks the finding is about, when it has no range or spans several.

fixAiIssueFixKind
choicesstring[]

The fixes to choose from (proofread’s replacements); the first is the default. "" deletes.

AiIssueFixKind

"none" | "tracked" | "direct". What fixing an issue does: nothing automatic, a tracked suggestion the user reviews, or a direct change track changes cannot represent (document configuration), which the user confirms first.

AiIssueFix

One issue to fix, with the chosen replacement when the issue offers choices.

NameTypeDefault
idstring
choicestring

AiIssueApplyResult

What a provider’s apply did. Ids it neither applied nor skipped count as gone.

NameTypeDefault
trTransaction | null

The fixes, on a transaction of the state apply received; null when nothing changed.

appliedstring[]
skipped{ id: string; reason: string; }[]

issueToolDeclarations

The issue tools’ declarations, for the chat route. Same order as createIssueTools.

NameTypeDefault
lengthnumber
toString() => string
toLocaleString{ (): string; (locales: string | string[], options?: (NumberFormatOptions & DateTimeFormatOptions) | undefined): string; }
concat{ (...items: ConcatArray<AiToolDeclaration>[]): AiToolDeclaration[]; (...items: (AiToolDeclaration | ConcatArray<AiToolDeclaration>)[]): AiToolDeclaration[]; }
join(separator?: string | undefined) => string
slice(start?: number | undefined, end?: number | undefined) => AiToolDeclaration[]
indexOf(searchElement: AiToolDeclaration, fromIndex?: number | undefined) => number
lastIndexOf(searchElement: AiToolDeclaration, fromIndex?: number | undefined) => number
every{ <S extends AiToolDeclaration>(predicate: (value: AiToolDeclaration, index: number, array: readonly AiToolDeclaration[]) => value is S, thisArg?: any): this is readonly S[]; (predicate: (value: AiToolDeclaration, index: number, array: readonly AiToolDeclaration[]) => unknown, thisArg?: any): boolean; }
some(predicate: (value: AiToolDeclaration, index: number, array: readonly AiToolDeclaration[]) => unknown, thisArg?: any) => boolean
forEach(callbackfn: (value: AiToolDeclaration, index: number, array: readonly AiToolDeclaration[]) => void, thisArg?: any) => void
map<U>(callbackfn: (value: AiToolDeclaration, index: number, array: readonly AiToolDeclaration[]) => U, thisArg?: any) => U[]
filter{ <S extends AiToolDeclaration>(predicate: (value: AiToolDeclaration, index: number, array: readonly AiToolDeclaration[]) => value is S, thisArg?: any): S[]; (predicate: (value: AiToolDeclaration, index: number, array: readonly AiToolDeclaration[]) => unknown, thisArg?: any): AiToolDeclaration[]; }
reduce{ (callbackfn: (previousValue: AiToolDeclaration, currentValue: AiToolDeclaration, currentIndex: number, array: readonly AiToolDeclaration[]) => AiToolDeclaration): AiToolDeclaration; (callbackfn: (previousValue: AiToolDeclaration, currentValue: AiToolDeclaration, currentIndex: number, array: readonly AiToolDeclarat...
reduceRight{ (callbackfn: (previousValue: AiToolDeclaration, currentValue: AiToolDeclaration, currentIndex: number, array: readonly AiToolDeclaration[]) => AiToolDeclaration): AiToolDeclaration; (callbackfn: (previousValue: AiToolDeclaration, currentValue: AiToolDeclaration, currentIndex: number, array: readonly AiToolDeclarat...
find{ <S extends AiToolDeclaration>(predicate: (value: AiToolDeclaration, index: number, obj: readonly AiToolDeclaration[]) => value is S, thisArg?: any): S | undefined; (predicate: (value: AiToolDeclaration, index: number, obj: readonly AiToolDeclaration[]) => unknown, thisArg?: any): AiToolDeclaration | undefined; }
findIndex(predicate: (value: AiToolDeclaration, index: number, obj: readonly AiToolDeclaration[]) => unknown, thisArg?: any) => number
entries() => ArrayIterator<[number, AiToolDeclaration]>
keys() => ArrayIterator<number>
values() => ArrayIterator<AiToolDeclaration>
includes(searchElement: AiToolDeclaration, fromIndex?: number | undefined) => boolean
flatMap<U, This = undefined>(callback: (this: This, value: AiToolDeclaration, index: number, array: AiToolDeclaration[]) => U | readonly U[], thisArg?: This | undefined) => U[]
flat<A, D extends number = 1>(this: A, depth?: D | undefined) => FlatArray<A, D>[]
at(index: number) => AiToolDeclaration | undefined
__@iterator@6565() => ArrayIterator<AiToolDeclaration>
__@unscopables@6567{ readonly [x: number]: boolean | undefined; readonly length?: boolean; toString?: boolean; toLocaleString?: boolean; concat?: boolean; join?: boolean; slice?: boolean; indexOf?: boolean; lastIndexOf?: boolean; ... 17 more ...; readonly [Symbol.unscopables]?: boolean; }

listIssuesDeclaration

NameTypeDefault
namestring
runsOn"editor"
kind"read"
deferfalse
labels{ running: string; complete: string; }
descriptionstring
inputSchemaZodObject<{ sources: ZodOptional<ZodArray<ZodString>>; severity: ZodOptional<ZodArray<ZodEnum<{ error: "error"; warning: "warning"; hint: "hint"; info: "info"; }>>>; fromIndex: ZodOptional<...>; }, $strip>

fixIssuesDeclaration

NameTypeDefault
namestring
runsOn"editor"
kind"edit"
deferfalse
labels{ running: string; complete: string; }
descriptionstring
inputSchemaZodObject<{ fixes: ZodArray<ZodObject<{ id: ZodString; choice: ZodOptional<ZodString>; }, $strip>>; }, $strip>

applyFormatFixesDeclaration

NameTypeDefault
namestring
runsOn"editor"
kind"config"
labels{ running: string; complete: string; confirm: string; }
descriptionstring
inputSchemaZodObject<{ ids: ZodArray<ZodString>; }, $strip>

Chat attachments

Reference documents sent with every chat request. The chat panel builds these itself; hosts that attach documents from elsewhere (a file store, another editor) use docToMarkdownAttachment.

docToMarkdownAttachment

Serialize an imported document (e.g. from @stesura/docx importDocx) into a markdown attachment. Plain profile on purpose: {#id}/num= attrs would reference blocks and list configs that don’t exist in the HOST document, which would mislead the editing tools.

Parameters:
NameTypeDefault
docNode
namestring
Returns:
{ attachment: AiChatAttachment; warnings: Warning[]; }

AiChatAttachment

{ kind: "markdown"; name; content; truncated?; hash? } | { kind: "pdf"; name; data; hash? }. Markdown attachments carry converted text; PDFs carry base64 (no data: prefix) for the model’s native PDF reading. hash (SHA-256 hex of the content) keys upload dedup: once the server confirms it holds a hash, later requests send an AiChatAttachmentRef ({ kind, name, hash, omitted: true }) instead of the bytes.

Limits

ConstantValueMeaning
MAX_ATTACHMENTS8Attachments per conversation, enforced client-side and by the server schema
ATTACHMENT_MARKDOWN_MAX_CHARS120_000Markdown characters per attachment before truncation (~30K tokens)
AI_ATTACHMENT_MAX_BASE64_CHARS30_000_000Shared size budget for all attachments, in base64 characters

Suggestions

Every AI edit lands as a track-changes suggestion. These dispatch and resolve them.

dispatchAsSuggestion

Dispatch an edit as track-changes suggestions, even when track changes is off (forceTrackChanges). skipTrackChanges is set on the tr BEFORE the transform — meta is copied onto the tracked tr wholesale, which stops the editor’s transaction modifier from transforming a second time on dispatch.

requestedBy is the signed-in user the agent works for, kept on every mark/attr. Pass trackChangesId to make several dispatches one suggestion (one per tool call).

Returns the trackChangesId stamped on every mark/attr of this edit, so the caller can later resolve exactly this suggestion (accept/reject scoped via resolveTrackChangesInRange’s filter).

Parameters:
NameTypeDefault
viewEditorView
trTransaction
userCurrentUser
requestedBy{ id: string; name?: string; }
trackChangesIdstring
Returns:
string

DEFAULT_AI_USER

Author of AI suggestions and comments when the host supplies no user; also the scope of “accept/reject all”.

NameTypeDefault
idstring
namestring
avatarstring
colorstring

hasAiSuggestion

True while the tracked edit of one tool call still exists in the document.

Parameters:
NameTypeDefault
stateEditorState
trackChangesIdstring
Returns:
boolean

resolveAiSuggestion

Accept or reject the tracked edit a single AI tool call produced, identified by the trackChangesId dispatchAsSuggestion returned. Returns false when nothing matched — the suggestion was already resolved from the document review UI.

Parameters:
NameTypeDefault
viewEditorView
trackChangesIdstring
actionAiSuggestionAction
Returns:
boolean

resolveAllAiSuggestions

Accept or reject every pending suggestion authored by the AI identity (all tool calls, all conversations); with requestedById, only those it made for that user, so one user’s accept all leaves another user’s agent edits pending (null: only those made for no one, what a chat without a signed-in user may resolve). Returns false when there were none.

Parameters:
NameTypeDefault
viewEditorView
actionAiSuggestionAction
userCurrentUser
requestedByIdstring
Returns:
boolean

scrollToAiSuggestion

Move the selection to the first block/mark of the tracked edit and scroll it into view. Returns false when the suggestion no longer exists.

Parameters:
NameTypeDefault
viewEditorView
trackChangesIdstring
Returns:
boolean

AiSuggestionAction

"approve" | "reject" — how resolveAiSuggestion and resolveAllAiSuggestions resolve.

Diagnostics

setAiLogger

Route the AI packages’ diagnostics (@stesura/ai, ai-react, ai-server) somewhere other than the console — a Sentry breadcrumb, a server log line. One process-wide sink, like console itself: these are operational diagnostics, not per-call results (those flow through return values and tool-result strings).

Parameters:
NameTypeDefault
loggerAiLogger
Returns:
void

AiLogger

Where the AI packages’ diagnostics go. Matches the console shape.

NameTypeDefault
warn(message: string, detail?: unknown) => void
error(message: string, detail?: unknown) => void
Last updated on