REFERENCE
Developer Reference
Authentication, APIs, SDKs, Flow Agent processors and parameter resolution, in full.
JavaScript SDK (@asgard-js/core)
@asgard-js/core JavaScript SDK reference
@asgard-js/core is the framework-agnostic JavaScript SDK. It handles the SSE stream, channel management and conversation state, and works in Node.js, in the browser, and anywhere you are not using the @asgard-js/react components (headless, Vue, Svelte).
With React you usually do not need @asgard-js/core directly. Use the <Chatbot> component from @asgard-js/react instead; see the React SDK reference.
Installation
npm install @asgard-js/coreCore concepts
Class What it is AsgardServiceClientConnection configuration (botProviderEndpoint, apiKey, customHeaders) ChannelOne conversation channel, talking to the Bot Provider over SSE ConversationThe container holding message state Basic usage
import { AsgardServiceClient, Channel, Conversation } from '@asgard-js/core';
const client = new AsgardServiceClient({
botProviderEndpoint:
'https://api.asgard-ai.com/ns/{namespace}/bot-provider/{botProviderId}',
});
const conversation = new Conversation({ messages: new Map() });
const channel = await Channel.reset({
client,
customChannelId: 'channel-123',
conversation,
statesObserver: (states) => {
console.log('isConnecting:', states.isConnecting);
console.log(
'Messages:',
Array.from(states.conversation.messages.values()),
);
},
});
await channel.sendMessage({ text: 'Hello!' });AsgardServiceClientconfigurationOption Type Required Description botProviderEndpointstringYes The Bot Provider's base URL apiKeystringNo API key (Direct Connect mode) customHeadersRecord<string, string>No Custom HTTP headers Channel and file operations
Beyond opening a connection,
AsgardServiceClientoffers these methods:Method Signature Description uploadFile(file: File, customChannelId: string) => Promise<BlobUploadResponse>Upload a file; the returned blob can be attached to a later message downloadChannelHomeFile(relativePath: string, customChannelId: string) => Promise<ChannelHomeDownloadResult>Download a file under that channel's home channelMetadata(customChannelId: string) => Promise<ChannelMetadata | null>Ask whether a channel exists; nullwhen it does notsuspendChannel(customChannelId: string, options?) => Promise<void>Stop the run currently executing on that channel deleteChannel(customChannelId: string) => Promise<void>Delete the channel detach(options: { timeoutMs: number }) => voidStop accepting new requests, then close once the in-flight ones finish close() => voidClose immediately detachandclosediffer in how they treat requests already in flight.closeshuts down straight away.detachmarks the client as no longer accepting new requests: if nothing is in flight it closes immediately, otherwise it waits for those requests to finish, which makes it the right choice when tearing down before a page unload.All of these need
botProviderEndpointto be configured, because each endpoint is derived from it. Without it they throw an error naming the endpoint that could not be derived.Message feedback
When a user rates one reply, send it with
sendMessageFeedback:import {
FEEDBACK_COMMENT_MAX_BYTES,
feedbackCommentByteLength,
composeFeedbackMessage,
} from '@asgard-js/core';
await client.sendMessageFeedback({
customChannelId,
messageId, // the messageId of the reply being rated
verdict: 'BAD', // 'GOOD' | 'BAD'
comment: 'This answers a different question than the one I asked',
});The returned
messageIdis the feedback entry's own id, distinct from the reply it rates. The response also carries the transcriptseq, the position at which a rejoining client sees this feedback replayed as anasgard.message.feedbackevent.The comment limit is 8 KiB, not 8192 characters
FEEDBACK_COMMENT_MAX_BYTESis8 * 1024, measured in UTF-8 bytes. A CJK character takes three bytes, so a length check in characters lets through a comment the server then rejects with400. Measure it withfeedbackCommentByteLength(comment):if (feedbackCommentByteLength(comment) > FEEDBACK_COMMENT_MAX_BYTES) {
// ask the user to shorten it
}Sending the feedback to the AI as well
composeFeedbackMessage(verdict, comment?)builds the follow-up message to send into the conversation: the prefix for that verdict, then, only when the user wrote something, a blank line and the comment verbatim.composeFeedbackMessage('BAD', 'This answers a different question');
// "[Response Feedback: Bad]\n\nThis answers a different question"RESPONSE_FEEDBACK_PREFIXholds those two prefix strings ([Response Feedback: Good]/[Response Feedback: Bad]). They are a byte-for-byte platform contract: the backend's system prompt recognises exactly these strings and uses them to treat the message as an interlude about the previous reply, acknowledging briefly, adjusting where the verdict was Bad, then continuing. Do not rewrite the prefix.Error handling
@asgard-js/coreexports three error classes and a type guard for each. All extendError, soinstanceofworks just as well as the guard.Class Type guard Thrown when HttpErrorisHttpErrorAn HTTP request returns a non-2xx status ChannelBusyErrorisChannelBusyErrorA run is already in flight on that channel ChannelAwaitingConsentErrorisChannelAwaitingConsentErrorThe channel is parked on a tool-call consent prompt import {
isHttpError,
isChannelBusyError,
isChannelAwaitingConsentError,
} from '@asgard-js/core';
try {
await channel.sendMessage({ text: input });
} catch (error) {
if (isChannelAwaitingConsentError(error)) {
// answer the consent prompt first; this turn cannot go out until then
} else if (isChannelBusyError(error)) {
// the previous turn has not finished, so keep the send button disabled
} else if (isHttpError(error) && error.status === 429) {
// read error.body to tell a spent quota from a rate limit
}
}HttpErrorCarries three readonly properties:
status,statusTextandbody.bodyis the decoded error payload, and in practice it is how you tell apart different causes behind one status code, for example readingreason_codeoff it to distinguish a spent quota from a plain rate limit on a429.bodymay beundefined. A response does not always carry JSON, and a failed parse is a normal outcome rather than a caller mistake, so check before reading it.ChannelBusyErrorChannel.sendMessage()rejects with this when a run already holds the channel. TherunKindproperty says which kind of run is holding it.Runs execute in the background on the server, so dispatching a second one would leave two runs writing to the same transcript. Rejecting rather than dropping the message silently is what lets the caller know it never left.
The SDK's own composer already gates this, so in practice you only see this error on a path where you send messages programmatically.
ChannelAwaitingConsentErrorChannel.sendMessage()andChannel.nudge()both reject with this while the channel is parked on a tool-call consent prompt. The server holds such a channel in a pause that only a consent reply resumes; every other turn is rejected, a nudge included.That pause is invisible to the run-in-flight check: the consent frame arrives after the run terminal, so by the time the prompt is on screen the run has already ended and the channel looks idle. Answer with
Channel.replyToolCallConsents()first, then send the next turn.The
processIdproperty is the paused batch's process id, but it is an empty string on a rejoin. A prompt recovered after a reload is rebuilt by the backend from durable pause state, and that path deliberately leaves the process id blank, because detecting a consent pause is a matter of the prompt's presence rather than this id. Treat it as a diagnostic hint, never as a key.Sandbox filesystem
The sandbox mounted for an agent run has a filesystem API, each method addressing a path within a named sandbox:
Method Signature sandboxFsList(sandboxName, path) => Promise<SandboxFsListResult>sandboxFsStat(sandboxName, path) => Promise<SandboxFsStatResult>sandboxFsRead(sandboxName, path, options?) => Promise<SandboxFsReadResult>sandboxFsWrite(sandboxName, path, ...) => Promise<void>sandboxFsMkdir(sandboxName, path) => Promise<void>sandboxFsRemove(sandboxName, path) => Promise<void>sandboxFsRemoveAll(sandboxName, path) => Promise<void>sandboxFsCopy(sandboxName, src, dst, ...) => Promise<void>sandboxFsMove(sandboxName, src, dst, ...) => Promise<void>sandboxFsWatch(sandboxName, path) => Observable<SandboxFsWatchEvent>sandboxFsWatchreturns an RxJSObservablerather than aPromise: once subscribed it keeps emitting change events for that path, so unsubscribe when you are done.generateSandboxBrowserOpenUrl(sandboxName)produces a URL that opens the sandbox in a browser.resolveSandboxUri(uri)parses a sandbox URI into aSandboxUriIntent, returningnullwhen it cannot.reconcileLaunched(metadata)tidies the list of launched sandboxes.On the React side you do not call these directly:
createSandboxFsProviderswraps them into the shape the file explorer expects. See the React SDK.SourceSet
AsgardSourceSetClientreads a SourceSet's contents. Its constants:Constant Value Description SOURCE_SET_MAX_PAGE_SIZE1000Maximum entries in one request SOURCE_SET_DEFAULT_MAX_ENTRIES10000Default cap on entries fetched SOURCE_SET_VOLUME_ROOT''The volume root, an empty string rather than /assertVolumePath(path, options?)validates a path and throws when it is not valid. The empty-string root is easy to get wrong, so use this function rather than comparing paths yourself.Events and derived data
fetchSse(payload, options?)opens the SSE connection and returns an RxJSSubscription.rejoinSse(customChannelId, options?)rejoins an existing channel and replays its transcript.handleEvent(response)feeds one SSE event into the client's internal state, and is normally called by the SDK itself.Helpers for deriving tasks and subagents from events:
Function Purpose isTaskTool(call)/isAgentTool(call)Whether a tool call is a task or a subagent isSubagentChildTool(parentToolUseId?)Whether a tool call sits under a subagent reduceTaskEvents(events)Reduce TaskToolEvent[]toTask[]reduceSubagents(events)Reduce SubagentEvent[]toSubagent[]conversationToSubagentEvents(messages)Extract subagent events from conversation messages tasksEqual(a, b)/subagentsEqual(a, b)Compare two lists, to avoid re-rendering when nothing changed Headless derived state (tasks, subagents, title)
Since 0.3.x,
@asgard-js/corealso exposes a framework-agnostic derived-state API, so headless, Vue or Svelte consumers can render the task list, subagent list and conversation title outside<Chatbot>:- Reactive stores on
Channel:channel.tasks$,channel.subagents$,channel.channelTitle$(RxJSObservables that emit only when that slice changes), plus the snapshot gettersgetTasks()/getSubagents()/getChannelTitle(). - Building your own from
conversation$:createDerivedStores(conversation$)returns{ tasks$, subagents$, getTasks, getSubagents, teardown };deriveTasks(conversation)andderiveSubagents(conversation)do a one-off derivation. - Joining an existing channel without clearing history:
Channel.restore(config, options?)first asksclient.channelMetadata()(GET /channel/metadata) whether the channel exists, and if it does replays the server transcript viaclient.rejoinSse()instead of sendingRESET_CHANNEL.
React consumersIn React, use
@asgard-js/react'suseTaskList/useSubagents/useChannelTitlehooks, which already bridge those stores intouseSyncExternalStore.- Reactive stores on
Sandbox browser
The browser running inside a Sandbox can be embedded in your own UI and driven there, instead of being opened in a new tab.
createSandboxBrowserSession(sandboxName)gets streaming credentials fromPOST {botProviderEndpoint}/sandbox/{sandbox_name}/browser/sessionand returns{ wsUrl, token }. Hand those tocreateSandboxBrowserTransportto open the connection; the screen and the input both travel over that WebSocket.This is a second route alongside
generateSandboxBrowserOpenUrl(sandboxName), not a replacement for it. That one hands you a one-time URL for the user to open in a new tab and is kept as the fallback; this one hands you a socket so the SDK can render the browser inside your page.The remote desktop protocol speaks X11 keysyms rather than the browser's
KeyboardEvent.key, so core also exports the mapping:Export What it does KEYSYMKeysym constants for the common keys charToKeysym(char)A single character to a keysym keyToKeysym(key)A KeyboardEvent.keyto a keysymmapModifierKeysym(...)Handles Shift / Ctrl / Alt and friends isPrintableKeysym(keysym)Whether a keysym is a printable character Wheel events differ in units between browsers, so
normalizeWheel(event)normalises them;WHEEL_MAXcaps a single scroll andWHEEL_THROTTLE_MSis the send interval. Remote cursor frames are decoded withdecodeCursorFrame.None of these low-level helpers has to be touched if you use the React SDK's
SandboxBrowserPanel— see React SDK.Interactive demo
Headless usage and event handling for
@asgard-js/core: SDK Demo — HeadlessFurther reading
- Frontend SDK guide — choosing an integration pattern, with the full walkthrough
- React SDK reference — the
@asgard-js/reactcomponents