@inlayai/sdk
The core client — InlayChatClient, transports, errors, and the row/timeline helpers.
The core client: the wasm-backed chat driver, the HTTP transport, the error taxonomy, and the row/timeline helpers. Framework-free — the React package is a thin reactive layer over this.
Every entry point takes the same ConnectionOptions
bag: a session, with baseUrl defaulting to production
(INLAY_PRODUCTION_URL) and a transport field
as the advanced seam (tests, non-browser hosts).
The client
createChat
createChat(opts: CreateChatOptions): InlayChatClient
InlayChatClient
One conversation against a deployed agent. The wasm reducer owns parse +
reduce and returns effects as data; the client is the driver — it opens
and pumps streams, runs tool handlers, delivers results, aborts, and cancels.
Construction warms the wasm module in the background; the first send()
joins the same init promise.
CreateChatOptions
CreateChatOptions extends ConnectionOptions —
{ agentId, session } is the whole happy path.
Prop
Type
Getters
| Getter | Type | Notes |
|---|---|---|
snapshot | ChatSnapshot | the FLAT top-level transcript (inner-subgraph rows dropped); no composite containers |
fullSnapshot | ChatSnapshot | the FULL path-tagged transcript (backs per-node joins) |
run | RunSnapshot | the topology projection |
streaming | boolean | an un-aborted run is in flight |
canSend | boolean | status map AND the synchronous latches — a gated composer never meets a rejection |
canCancel | boolean | something live to stop (streaming, or the send/reopen startup windows) |
canRetry | boolean | failed + retryable, nothing in flight |
Methods
| Method | Semantics |
|---|---|
send(text, opts?: { throwOnFailure?: boolean }) | Starts or resumes a turn; resolves when the stream closes. Rejects InlayRunError on failure by default (headless is reject-by-default, unlike React); throwOnFailure: false relaxes to resolve. Misuse (a turn/reopen in flight, status awaitingTool) rejects in every mode — promise rejections, not sync throws. |
retry() | Re-drives the failed node (POST …/retry); rejects like send on re-failure (the conversation stays retryable — the cursor survives). |
cancel() | Stops the turn (streaming or paused); a no-op when idle. |
reset() | Abandons the conversation to idle/empty (synchronous); the next send starts fresh. |
resume(id, opts?) | Reopens a persisted conversation (hydrate); opts pages the window. Idempotent; refuses a foreign-agent id unless { allowForeignAgent: true } — then READ-ONLY (the transcript hydrates; send/retry/submitToolResult refuse, canSend/canRetry read false — the multi-agent-sidebar affordance; loadEarlier pages, delete tombstones). A same-id resume while streaming is a no-op; a superseded attempt's failure resolves silently (the newest attempt reports its own truth). |
submitToolResult(toolCallId, content) | Delivers a durable tool result and resumes. A no-op for an unknown id; rejects while a reopen/page is in flight. |
loadEarlier(limit?) | Pages the reopened log backward; resolves to the number of rows added (0 at the start). Rejects while streaming or without a prior resume. |
Transport
Transport
The HTTP surface the client drives — an interface so tests can inject a mock
and non-browser hosts can swap fetch.
| Method | Endpoint | Notes |
|---|---|---|
openExecuteStream(agentId, body, signal?) | POST /v1/agents/:id/execute/stream | fresh run, returns the SSE stream |
openResumeStream(conversationId, content, signal?) | POST /v1/conversations/:id/messages | continue with a user message |
openRetryStream(conversationId, signal?) | POST /v1/conversations/:id/retry | no body; returns the resumed continuation |
postToolResult(conversationId, toolCallId, content, signal?) | POST …/tool-results | delivers a result AND resumes — returns a stream, not a bare POST |
cancel(conversationId) | DELETE /v1/conversations/:id | 404 is the API's masked "nothing to cancel" — not an error |
fetchConversation?(conversationId, opts?) | GET /v1/conversations/:id | raw JSON for the wasm hydrate; optional — without it resume/loadEarlier throw a clear error |
listConversations?(query?) | GET /v1/conversations/mine | parsed summaries; optional — without it listConversations throws |
deleteConversation?(conversationId) | POST /v1/conversations/:id/delete | soft-delete from history; optional — without it deleteConversation throws. 404 (already gone) tolerated |
HttpTransport
Implements Transport over fetch: resolves the bearer token from
session, refreshes and retries ONCE on a 401 (provider-backed sessions
only), and retries transient network rejections with exponential backoff
(HTTP error responses are never retried).
HttpTransportOptions
Prop
Type
ConnectionOptions
The connection bag every entry point accepts. Just session in the common
case — the API origin defaults to production. A given transport wins over
session/baseUrl (the test seam).
Prop
Type
INLAY_PRODUCTION_URL
"https://app.inlayai.com" — the default baseUrl of every entry point in
all three packages. Override only for a self-hosted or staging deployment.
History — listConversations
listConversations(conn: ConnectionOptions, query?: ConversationListQuery): Promise<ConversationSummary[]> — a thin stateless wrapper (throws if the
transport doesn't implement it). The snake→camel mapping of
ConversationSummary happens here — the one REST DTO the client consumes;
everything else JS-facing crosses the wasm boundary already camel.
deleteConversation(conn: ConnectionOptions, conversationId: string): Promise<void> — remove one conversation from the caller's history (a
soft-delete tombstone server-side). Idempotent — deleting an already-gone
conversation is not an error. In React, prefer useInlayHistory's
deleteConversation(id) (it also drops the row from the list optimistically).
Throws a clear error if the transport omits deleteConversation.
Built-in tool names
The platform's INTRINSIC tools reach the client as timeline tool-call rows
carrying a toolName (with requiresClientResponse: false — observability
only). Client code that special-cases a built-in — rendering a code.exec call,
reacting to a memory.remember — should reference these constants instead of
magic strings. Agent-DECLARED client tools are typed per-agent by inlay codegen (<Base>ToolName = keyof <Base>ToolHandlers); these are the platform
intrinsics any agent might invoke. They're pinned to the Rust source of truth by
a test, so a rename on either side fails the build.
| Constant | Wire name |
|---|---|
MEMORY_TOOL | object — PEEK, LOAD, REMEMBER, UNLOAD, FORGET, LIST, APPEND, REPLACE, LINK, UNLINK, RESURFACE, RECALL → memory.<op> |
CODE_EXEC_TOOL | "code.exec" |
SCRATCHPAD_TOOL | "scratchpad" |
RETRIEVAL_SEARCH_TOOL | "retrieval.search" |
WEB_SEARCH_TOOL | "web.search" (only when a search key is configured) |
URL_FETCH_TOOL | "url.fetch" |
ARTIFACT_EDIT_TOOL | "artifact.edit" |
The dynamic prefixes (agent.<alias>, mcp.*) are per-tenant/per-alias, not
fixed names, so they're deliberately not constants here.
Capabilities
Capabilities
capabilities(status: ChatStatus): Capabilities — the pure
(status → allowed) map, so apps drive buttons off semantics instead of
re-deriving !streaming math (which gets awaitingInput wrong). For
canSend this is the status HALF only — InlayChatClient.canSend ANDs the
synchronous latches, and is the only safe composer gate.
canSend = status ∉ awaitingTool — note the terminal
statuses allow it, where send starts a FRESH conversation.
canCancel = a live run (streaming, plus the synchronous starting/reopen windows the status map can't see). An idle row is continuable, not stoppable — delete() is the permanent end.
reset has no flag — valid in every status.
Prop
Type
Errors
Every error the SDK raises derives from InlayError.
InlayError
The base class for every SDK error.
InlayRunError
{ code: InlayErrorCode; reason?: PublicError } — a run failed
(code: "run_failed") or the stream died opening/pumping
(code: "open_failed"). Deliberately ONE rejection type, so
try { await send() } catch catches the network and the agent. reason
is the customer-safe cause (absent when no node reported one). Match on
code, never the message.
InlayErrorCode
"run_failed" | "open_failed".
InlayHttpError
{ status: number; body: string } — a non-2xx API response. Getters:
isAuth (401), isForbidden (403), isNotFound (404), isConflict (409),
isQuotaExceeded (429 billing denial — do not auto-retry; render your
own upgrade CTA), isRateLimited (429 transient throttle — honor
Retry-After), and quota (the parsed figures, below).
QuotaDetails
{ limitMicros?, usedMicros?, limitTokens?, usedTokens?, resetsAt? } — the
structured extension members of a quota_exceeded 429, parsed from the
problem body (resetsAt as a Date). null from the quota getter for
any other error or a pre-7A server — version tolerance by construction.
Fields are present by quota kind: cost denials carry the micros pair, token
denials the tokens pair. resetsAt is the absolute moment the billing
period opens, not a retry delay.
Billing (end-user reads)
Typed reads and hosted payment flows over /v1/billing/*, scoped by the
session JWT. Your app renders its own pricing page, wallet, and upgrade CTA
from these figures. These are your users' billing data, not Inlay's commercial
pricing. See the billing guide.
BillingFacade
Also reachable as chatClient.billing.
| Method | Returns and behavior |
|---|---|
balance() | Promise<BillingBalance>: the credit wallet decomposition. |
usage() | Promise<BillingUsage>: current-period usage and allowance, using the gate's server-side counter figures. |
plans() | Promise<BillingPlans>: the tenant's published catalog and currentPlanId. |
invoices() | Promise<BillingInvoice[]>: the end user's own invoices, newest first. |
topup(amountUsd) | Promise<HostedCheckout>: a hosted invoice URL for a credit top-up. The argument is USD, not micro-USD; credits arrive after payment via webhook. |
subscribeCheckout({ planId, successUrl, cancelUrl }) | Promise<HostedCheckout>: hosted checkout for a new subscription to a published plan. |
Both mutations throw if the transport returns no hosted URL. Redirect to the
returned URL; do not collect card data yourself. They require a connected
Stripe account with charges enabled; unconfigured accounts receive 409.
Subscribe checkout is for new subscriptions only, not plan changes for an
already-subscribed user. See
hosted subscribe and top-up.
createBilling
createBilling(conn: ConnectionOptions): BillingFacade — construct the facade
standalone (a pricing page doesn't need a chat client): { session } suffices.
Retrieval (built-in RAG)
The retrieval surface (the built-in-RAG feature): ingest + query + aggregate
- the index profile + the eval pack, against the
/v1/retrieval/indexes/:ns/*routes. The ACL + per-user-namespace enforcement is server-side — the client never names the reserved acl field and never passes a principal.
RetrievalFacade
The retrieval client. Construct via createRetrieval({ session }).
| Method | Returns |
|---|---|
upsertDocuments(ns, documents) | Promise<IngestStats> — ingest a batch (management tier) |
deleteDocuments(ns, sources) | Promise<number> — delete every chunk of the given sources |
query(ns, text, opts?) | Promise<RetrievalHit[]> — top-k with filters/include/diversify |
queryBatch(ns, queries) | Promise<RetrievalHit[][]> — ≤16 subqueries; the atomic reserve-N 429 contract |
aggregate(ns, aggregateBy, groupBy, filters?) | Promise<AggregateRow[]> — a GROUP BY over the declared metadata |
getProfile(ns) | Promise<ProfileInfo> — the effective profile + the legacy flag |
putProfile(ns, profile, opts?) | Promise<ProfileUpdate> — the evolution classification + status |
previewProfile(ns, profile) | Promise<ProfilePreview> — the evolution a put WOULD produce, without applying |
getEvalPack(ns) | Promise<EvalPack | null> — the golden set, if any |
putEvalPack(ns, pack) | Promise<void> — install the golden set for that index's corpus-publication gate, not every agent publish |
evaluate(ns) | Promise<EvalReport> — run the pack against the live index |
stats(ns) | Promise<RetrievalStats> — chunk counts, dim, the pinned status |
createRetrieval
createRetrieval(conn: ConnectionOptions): RetrievalFacade — construct the
retrieval facade standalone.
Operator triggers (schedules + run-as)
The Phase-2.5 surface: fire agent runs SERVER-SIDE, with no end-user browser
session — cron-scheduled runs (the schedules methods) and the on-demand
runAs. Both run the agent AS a named end user (their durable memory, usage,
and recall scope to them — the AuthContext::System principal). These are
MANAGEMENT-tier calls — an operator api-key / dashboard credential, never a
customer session. Pass the operator credential as the session:
createOps({ session: process.env.INLAY_SECRET_KEY! }).
OpsFacade
The operator-trigger client. Construct via createOps({ session }).
| Method | Returns |
|---|---|
createSchedule(agentId, input) | Promise<Schedule> — a cron schedule (its first nextRunAt is computed). input.version pins it to an immutable agent version |
listSchedules(agentId) | Promise<Schedule[]> — an agent's schedules, oldest-first |
getSchedule(agentId, scheduleId) | Promise<Schedule> — one schedule |
setScheduleEnabled(agentId, scheduleId, enabled) | Promise<Schedule> — pause/resume |
updateSchedule(agentId, scheduleId, input) | Promise<Schedule> — edit the config (cron/timezone/task/enabled + the version pin). Only the present fields change; a cron change recomputes nextRunAt; version is a tri-state (absent/null/number) |
deleteSchedule(agentId, scheduleId) | Promise<void> — delete |
listTenantSchedules(opts?) | Promise<Schedule[]> — the tenant-wide list. ADMIN-only: spans every act-as userId + task; never render to end users. opts.limit/opts.offset page it (default 50, max 200) |
listMySchedules(opts?) | Promise<MySchedule[]> — the end-user-safe list (only the session user's own) — safe to render to end users. opts.limit/opts.offset page it |
runAs(agentId, { userId, task?, version? }) | Promise<RunAsResult> — fire a run AS the named user (sync, a fresh conversation). version pins it |
createOps
createOps(conn: ConnectionOptions): OpsFacade — construct the facade standalone.
Schedule
One schedule: cron + timezone, the run task (the kickoff instruction —
seeded as the run's first user message), the act-as userId, enabled, the
computed nextRunAt / lastRunAt, the consecutiveFailures circuit-breaker
count (auto-disables past 5), and the optional pinnedVersion (absent = tracks
latest-published; a number = pinned to that immutable agent version, so a
publish/rollback doesn't move the schedule).
MySchedule
The END-USER-SAFE schedule view (the listMySchedules result) — a whitelist
over the internal schedule: id, agentId, cron, timezone,
pinnedVersion, enabled, nextRunAt / lastRunAt, consecutiveFailures,
createdAt. It has NO task (the kickoff) and no lease/tenant bookkeeping —
safe to render to end users.
CreateScheduleInput
createSchedule's input: cron (standard 5-field), optional timezone
(default UTC), the task kickoff, the act-as userId, optional enabled,
optional version (pin to an immutable agent version; validated to exist).
UpdateScheduleInput
updateSchedule's input — a partial config edit (only the present fields
change): cron, timezone, task, enabled, and version as a tri-state
(absent = unchanged, null = clear the pin, a number = pin). A cron/
timezone change recomputes the next fire time. agentId/userId aren't
editable.
RunAsResult
The fired run's conversation: conversationId, status, optional output.
RetrievalDocument
One document to ingest: text + optional id, kind (doc | row),
source, heading, metadata (validated against the index's profile).
RetrievalHit
One retrieval hit. scoreKind marks what distance carries (l2 / sq8 /
rrf / rerank). generation is the corpus marker for citation drift
detection. highlights are matched-fragment byte offsets into text (when
the query asked for them via include: ["highlights"]).
RetrievalQueryOptions
Query shaping: k, nprobe, contextChars, kind, exact, filters
(the positional filter tree), include (["highlights"]).
RetrievalProfile
The index's declared profile (the closed schema, rag.md §4): version,
embedder, plus optional chunker, contextual_prefix, ivf, metadata,
retrieval, answer_policy, management_acl.
The retrieval field holds the query-time knobs: bm25 (the lexical half),
fusion (the hybrid mode + weights), rerank ({ kind: "none" | "api", model? } — a reranker pass over the fused candidates), diversify
({ attribute, limit } — an MMR-style spread over a declared metadata field).
EvalPack
The eval pack: cases (golden queries + expectSources / expectNone) +
minRecall + maxFalseHitRate — the thresholds that index's corpus-publication
gate enforces when a pack is installed. This is not an agent-publish eval.
EvalReport
The eval report: total, passed, recall, falseHitRate, gatePassed,
and per-case results.
Supporting types
IngestStats (the ingest result), AggregateSpec (["Count"] /
["Sum", field]), AggregateRow, EvalCase, EvalCaseResult,
RetrievalStats, ProfileInfo, ProfileUpdate, ProfilePreview,
BatchResults — see the facade method signatures above.
Citations (grounded answers)
A grounded answer's citations (rag.md §5.4) ride the message.end payload
into the assistant row's citations. A citation is a point-in-time
snapshot — generation + snippet let a frontend detect corpus drift
after a re-publish (chunk ids renumber).
Citation
One claim's backing passage: claim, chunkId, source, snippet, plus
optional heading + generation.
Helpers
| Function | Returns |
|---|---|
citationsOf(row) | the row's citations ([] for a non-grounded turn) |
allCitations(rows) | every row's citations, in order |
citationDrifted(citation, currentGeneration) | whether the corpus re-published since the answer |
partitionByDrift(citations, currentGeneration) | { current, drifted } |
citationMarkers(citations, currentGeneration?) | CitationMarker[] — the 1-based [n] footnote markers + the drift flag |
CitationMarker
A renderable footnote marker: marker (the 1-based [n]), citation, and
drifted.
Row guards and accessors
All take Row and are re-exported from @inlayai/react (one front door).
| Function | Returns |
|---|---|
isUserRow / isAssistantRow / isOutputRow / isToolCallRow / isToolResultRow | type guards (row is …) |
hasReasoning(row) | row is AssistantRow & { reasoning: string } — narrows the text non-optional |
rowText(row?) | user/assistant content, else "" (undefined-safe) |
rowReasoning(row?) | assistant reasoning, else "" |
rowComplete(row) | false only for streaming assistant/output rows — append-only kinds are complete on arrival |
transcript(rows) | top-level (UserRow | AssistantRow)[] — the flat chat view (pinned equal to the index's) |
outputsNamed(rows, name) | the output rows for one name, oldest first — the per-turn production history bindings can't answer |
The timeline index
Indexes over the ordered row array, built ONCE per commit with
reference-preservation: a slice whose contents didn't change keeps its object
identity, so a useSyncExternalStore consumer keyed on one path re-renders
only for that path. Build it at the commit (as the store does) — building
one per render is the exact anti-pattern it exists to prevent.
buildTimelineIndex
buildTimelineIndex(rows, previous?): TimelineIndex
deriveFeed
deriveFeed(rows, run, config, previous, hasEarlier?): FeedRow[] composites
the full row array into FanOutRow,
SubagentRow, and
RepeatRow containers per the
static config, threading previous for reference stability. hasEarlier
takes the snapshot's windowed-history flag. Passing null as config returns
the supplied rows flat, including inner rows. This helper does not check the
conversation's agent or version; the generated React path performs that
guard before composition. Pacing and
reference threading remain client-side. Plumbing, not the
front door: the generated hook calls it for you via useInlayChat's
machinery. The headless client's snapshot.rows and raw useInlayChat
without generated graph config stay flat.
computeFeedConfig
computeFeedConfig(nodes, regions, subagentPaths): FeedConfig normalizes the
generated module's NODES/REGIONS/SUBAGENTS into the feed's static inputs.
regions is a required readonly FeedRegion[], not an edge table. Node
kind and ensemble samples supply repeat metadata; subagent paths supply
containers. The generated hook supplies these automatically, and the machinery
memoizes one per generated config; apps using codegen do not configure feeds
manually.
There is no SDK graph prover. Descriptors are authoritative, including
[], which means no fan-out grouping even if NODES describes a Collect
router. Missing region metadata in an older generated module also does not
trigger inference from NODES/EDGES; regenerate the module.
EDGES remains available for selectBranches.
FeedConfig
The normalized static inputs returned by computeFeedConfig:
| Member | Meaning |
|---|---|
regions | FeedRegion[]: a copy of the authoritative descriptor list; normalization does not discover or re-prove regions. |
subgraphs | The absolute paths of the graph's subgraph nodes. |
repeatables | A map keyed by absolute node path (/-joined). An ensemble has mode: "parallel" and optional total from its generated samples (the static sample count); a for_each_llm has mode: "sequential" and no static total. |
Repeat-only graphs still populate repeatables and use the feed pipeline,
even when regions and subgraphs are empty. See
RepeatRow for the resulting row's
grouping and progress semantics.
FeedRegion
One authoritative fan-out descriptor in the generated
REGIONS export:
Prop
Type
source, join, and each branchEntries entry are absolute node-id paths.
branchEntries follows branch declaration order; branchNodes is parallel
to it, with each branch represented by sorted slash-joined absolute path
strings, excluding the selected join. A direct branch to the join has an
empty membership list, not an omitted branch.
Generated descriptors always include fromPort: a string for a single-port
fan-out, or null for a Collect group across rule ports. else and error
fallback targets are not parallel tracks. The projection keeps the shared
analyzer's selected join and declines ambiguous fallback ingress; it does not
find a different join for the rule-only subset. This is static presentation
metadata, not runtime firing provenance or an execution-policy declaration.
TimelineIndex
Prop
Type
| Function | Returns |
|---|---|
rowPath(row) | the row's absolute path, /-joined ("" = top level) |
rowsAtPath(index, path) | the indexed slice (reference-stable; shared empty on miss) |
rowsUnder(index, path) | rows at OR BELOW a path — the container rollup |
latestAssistantAt(index, path) | newest assistant row at a path (subscribes to the whole index — prefer latestAssistantIn) |
latestAssistantIn(rows) | newest assistant row in an already-scoped slice |
assistantsInActivation(rows) | ALL assistant rows of the current activation (splits from artifacts and server tool loops stay joined) |
Validation, wasm, escape hatches
validateArgs(value, schema): string | null — the tolerant JSON-Schema
subset validator (unknown constructs pass) used for tool args at the client
boundary and for settled binding values.
ensureWasmReady(input?: WasmInput): Promise<void> — idempotent wasm init
(Node reads from disk; the browser fetches relative to the module).
WasmInput: BufferSource | URL | Response | WebAssembly.Module — pass it
when your bundler can't resolve the default .wasm URL.
StreamPacer — the wasm reveal pacer the smoothing hooks share, exported as
an escape hatch. Almost nothing should touch it directly.