JavaScript SDK (@asgard-js/core)
@asgard-js/core はフレームワーク非依存の JavaScript SDK です。SSE ストリーム、Channel の管理、Conversation の状態を扱います。Node.js、ブラウザ、そして @asgard-js/react のコンポーネントを使わない場面(headless、Vue、Svelte など)で利用できます。
React では通常 @asgard-js/core を直接使う必要はありません。@asgard-js/react の <Chatbot> コンポーネントをご利用ください。React SDK リファレンスを参照してください。
インストール
npm install @asgard-js/core
中心となる概念
| クラス | 説明 |
|---|---|
AsgardServiceClient | 接続設定(botProviderEndpoint、apiKey、customHeaders) |
Channel | 一つの対話チャネル。SSE で Bot Provider と通信します |
Conversation | メッセージの状態を保持するコンテナ |
基本的な使い方
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: 'こんにちは!' });
AsgardServiceClient の設定
| 項目 | 型 | 必須 | 説明 |
|---|---|---|---|
botProviderEndpoint | string | はい | Bot Provider のベース URL |
apiKey | string | いいえ | API キー(Direct Connect モード) |
customHeaders | Record<string, string> | いいえ | 独自の HTTP ヘッダー |
チャネルとファイルの操作
接続の確立以外に、AsgardServiceClient は次のメソッドを提供します。
| メソッド | シグネチャ | 説明 |
|---|---|---|
uploadFile | (file: File, customChannelId: string) => Promise<BlobUploadResponse> | ファイルをアップロードし、返る blob 情報を後続のメッセージに添付できます |
downloadChannelHomeFile | (relativePath: string, customChannelId: string) => Promise<ChannelHomeDownloadResult> | そのチャネルの home 配下のファイルをダウンロードします |
channelMetadata | (customChannelId: string) => Promise<ChannelMetadata | null> | チャネルの存在を確認します。存在しない場合は null |
suspendChannel | (customChannelId: string, options?) => Promise<void> | そのチャネルで実行中の run を中止します |
deleteChannel | (customChannelId: string) => Promise<void> | チャネルを削除します |
detach | (options: { timeoutMs: number }) => void | 新規リクエストの受付を止め、実行中のものが終わってから閉じます |
close | () => void | 直ちに閉じます |
detach と close の違いは実行中のリクエストの扱いです。close はすぐに閉じます。detach は新規受付を止め、実行中のものがなければ即座に閉じ、あれば終わるまで待ちます。ページを離れる前の後始末に向いています。
いずれも botProviderEndpoint の設定が必要です。各エンドポイントはそこから導出されるためで、未設定の場合はどのエンドポイントの導出に失敗したかを示すエラーが発生します。
メッセージへのフィードバック
ユーザーが一つの返信を評価したら、sendMessageFeedback で送信します。
import {
FEEDBACK_COMMENT_MAX_BYTES,
feedbackCommentByteLength,
composeFeedbackMessage,
} from '@asgard-js/core';
await client.sendMessageFeedback({
customChannelId,
messageId, // 評価対象の返信の messageId
verdict: 'BAD', // 'GOOD' | 'BAD'
comment: '質問した内容とは別のことに答えています',
});
返る messageId はこのフィードバック自身の id で、評価された返信のものとは異なります。あわせて transcript の seq が返り、チャネルに再参加した際にその位置で asgard.message.feedback イベントとして再生されます。
コメントの上限は 8192 文字ではなく 8 KiB
FEEDBACK_COMMENT_MAX_BYTES は 8 * 1024 で、単位は UTF-8 のバイト数です。日本語や中国語の文字は 3 バイトを占めるため、文字数で検査するとサーバーが 400 で拒否するコメントを通してしまいます。feedbackCommentByteLength(comment) で測ってください。
if (feedbackCommentByteLength(comment) > FEEDBACK_COMMENT_MAX_BYTES) {
// 短くするようユーザーに促す
}
フィードバックを AI にも伝える
composeFeedbackMessage(verdict, comment?) は対話に送る後続メッセージを組み立てます。先頭はその verdict に対応する接頭辞、ユーザーがコメントを書いた場合のみ空行を挟んで原文が続きます。
composeFeedbackMessage('BAD', '質問した内容とは別のことに答えています');
// "[Response Feedback: Bad]\n\n質問した内容とは別のことに答えています"
RESPONSE_FEEDBACK_PREFIX はその 2 つの接頭辞文字列([Response Feedback: Good] / [Response Feedback: Bad])です。これらはバイト単位のプラットフォーム規約で、バックエンドの system prompt がこの文字列そのものを認識し、直前の返信についての差し込みとして扱います。接頭辞を書き換えないでください。
エラー処理
@asgard-js/core は 3 つのエラークラスと、それぞれの type guard を公開しています。いずれも Error のサブクラスなので、instanceof でも guard でも判定できます。
| クラス | Type guard | 発生する状況 |
|---|---|---|
HttpError | isHttpError | HTTP リクエストが 2xx 以外を返した |
ChannelBusyError | isChannelBusyError | そのチャネルで既に run が実行中 |
ChannelAwaitingConsentError | isChannelAwaitingConsentError | チャネルがツール呼び出しの同意プロンプトで停止している |
import {
isHttpError,
isChannelBusyError,
isChannelAwaitingConsentError,
} from '@asgard-js/core';
try {
await channel.sendMessage({ text: input });
} catch (error) {
if (isChannelAwaitingConsentError(error)) {
// 先に同意プロンプトへ回答しないと、このターンは送信できません
} else if (isChannelBusyError(error)) {
// 前のターンが未完了。送信ボタンを無効のままにするなど
} else if (isHttpError(error) && error.status === 429) {
// error.body で残高切れかレート制限かを判別します
}
}
HttpError
status、statusText、body の 3 つの読み取り専用プロパティを持ちます。body は解析済みのエラー内容で、実務では同じステータスコードの中で原因を見分ける手掛かりになります。たとえば 429 のとき reason_code を読んで、残高切れか単なるレート制限かを区別します。
body は undefined になり得ます。レスポンスが必ず JSON を伴うとは限らず、解析の失敗は呼び出し側の誤りではなく通常の結果なので、読む前に確認してください。
ChannelBusyError
Channel.sendMessage() は、そのチャネルで既に run が実行中のときこのエラーで reject します。runKind プロパティがどの種類の run が占有しているかを示します。
run はサーバー側でバックグラウンド実行されるため、もう一つ送ると同じ対話記録に 2 つの run が書き込むことになります。黙って破棄せず明確に拒否することで、呼び出し側はメッセージが送られていないと分かります。
SDK 内蔵の入力欄は既にこの状況を防いでいるため、実際にこのエラーが出るのはプログラムからメッセージを送る経路だけです。
ChannelAwaitingConsentError
Channel.sendMessage() と Channel.nudge() は、チャネルがツール呼び出しの同意プロンプトで停止しているときこのエラーで reject します。サーバーはそのチャネルを一時停止状態に保ち、同意への回答だけが再開させます。それ以外のターンは nudge も含めて拒否されます。
この一時停止は「run 実行中」の判定では検出できません。同意のフレームは run の終了後に届くため、プロンプトが画面に出た時点で run は既に終わっており、チャネルはアイドルに見えます。まず Channel.replyToolCallConsents() で回答し、その後に次のターンを送ってください。
processId プロパティは停止中のバッチの process id ですが、既存チャネルへの再参加時は空文字列になります。リロード後に復元されたプロンプトはバックエンドが永続化された停止状態から再構築し、その経路では process id を意図的に空にしています。同意による停止かどうかはプロンプトの有無で判断するためです。診断の手掛かりとして扱い、キーには使わないでください。
Sandbox のファイルシステム
Agent の実行時にマウントされる Sandbox にはファイル操作 API があり、いずれも sandboxName とパスで対象を指定します。
| メソッド | シグネチャ |
|---|---|
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> |
sandboxFsWatch は Promise ではなく RxJS の Observable を返します。購読するとそのパスの変更イベントを受け取り続けるので、不要になったら unsubscribe してください。
generateSandboxBrowserOpenUrl(sandboxName) はブラウザでその Sandbox を開く URL を生成します。
resolveSandboxUri(uri) は Sandbox URI を SandboxUriIntent に解析し、解析できない場合は null を返します。reconcileLaunched(metadata) は起動済み Sandbox の一覧を整理します。
React 側でこれらを直接呼ぶ必要はありません。createSandboxFsProviders がファイルエクスプローラーの求める形にまとめます。React SDK を参照してください。
SourceSet
AsgardSourceSetClient は SourceSet の内容にアクセスします。関連する定数は次のとおりです。
| 定数 | 値 | 説明 |
|---|---|---|
SOURCE_SET_MAX_PAGE_SIZE | 1000 | 1 リクエストあたりの最大件数 |
SOURCE_SET_DEFAULT_MAX_ENTRIES | 10000 | 取得件数の既定の上限 |
SOURCE_SET_VOLUME_ROOT | '' | volume のルート。/ ではなく空文字列です |
assertVolumePath(path, options?) はパスの妥当性を検証し、不正な場合はエラーを送出します。ルートが空文字列である点は誤りやすいので、自前で比較せずこの関数を使ってください。
イベントと派生データ
fetchSse(payload, options?) は SSE 接続を確立し、RxJS の Subscription を返します。rejoinSse(customChannelId, options?) は既存チャネルに再参加し transcript を再生します。handleEvent(response) は単一の SSE イベントを client の内部状態に取り込みます。通常は SDK 自身が呼び出します。
イベントからタスクとサブエージェントを導出するための関数です。
| 関数 | 用途 |
|---|---|
isTaskTool(call) / isAgentTool(call) | そのツール呼び出しがタスクかサブエージェントかを判定します |
isSubagentChildTool(parentToolUseId?) | サブエージェント配下のツール呼び出しかを判定します |
reduceTaskEvents(events) | TaskToolEvent[] を Task[] にまとめます |
reduceSubagents(events) | SubagentEvent[] を Subagent[] にまとめます |
conversationToSubagentEvents(messages) | 対話メッセージからサブエージェントのイベントを抽出します |
tasksEqual(a, b) / subagentsEqual(a, b) | 2 つの一覧を比較し、不要な再描画を避けます |
Headless の派生状態(タスク / サブエージェント / タイトル)
0.3.x 以降、@asgard-js/core はフレームワーク非依存の派生状態 API も提供します。headless、Vue、Svelte などの利用者が <Chatbot> の外でタスク一覧、サブエージェント一覧、対話タイトルを描画できます。
Channel上のリアクティブな store:channel.tasks$、channel.subagents$、channel.channelTitle$(該当部分が変化したときだけ発行する RxJS のObservable)と、対応するスナップショット取得のgetTasks()/getSubagents()/getChannelTitle()。conversation$から自分で構築する:createDerivedStores(conversation$)は{ tasks$, subagents$, getTasks, getSubagents, teardown }を返します。一度きりの導出にはderiveTasks(conversation)/deriveSubagents(conversation)を使います。- 履歴を消さずに既存チャネルへ参加する:
Channel.restore(config, options?)はまずclient.channelMetadata()(GET /channel/metadata)でチャネルの存在を確認し、存在すればRESET_CHANNELを送らずにclient.rejoinSse()でサーバーの transcript を再生します。
React では @asgard-js/react の useTaskList / useSubagents / useChannelTitle フックを使ってください。上記の store を useSyncExternalStore に橋渡し済みです。
Sandbox ブラウザー
Sandbox 内で動くブラウザーは、新しいタブを開かずに自分の画面へ埋め込んで操作できます。
createSandboxBrowserSession(sandboxName) は POST {botProviderEndpoint}/sandbox/{sandbox_name}/browser/session からストリーミング用の資格情報を取得し、{ wsUrl, token } を返します。それを createSandboxBrowserTransport に渡して接続すると、画面も入力もその WebSocket を通ります。
これは既存の generateSandboxBrowserOpenUrl(sandboxName) の置き換えではなく、もう 1 つの経路です。あちらは新しいタブで開くための一度きりの URL を返し、フォールバックとして残っています。こちらは SDK がページ内で描画するためのソケットを返します。
リモートデスクトップのプロトコルはブラウザーの KeyboardEvent.key ではなく X11 keysym を使うため、core は変換ユーティリティも公開しています。
| エクスポート | 役割 |
|---|---|
KEYSYM | よく使うキーの keysym 定数 |
charToKeysym(char) | 1 文字を keysym へ |
keyToKeysym(key) | KeyboardEvent.key を keysym へ |
mapModifierKeysym(...) | Shift/Ctrl/Alt などの修飾キーの処理 |
isPrintableKeysym(keysym) | 印字可能な文字かどうか |
ホイールイベントは単位がブラウザーごとに違うので normalizeWheel(event) で正規化します。WHEEL_MAX は 1 回のスクロール量の上限、WHEEL_THROTTLE_MS は送信の間隔です。リモートカーソルのフレームは decodeCursorFrame でデコードします。
React SDK の SandboxBrowserPanel を使う場合、これらの低レベルユーティリティに直接触れる必要はありません。React SDK を参照してください。
インタラクティブなデモ
@asgard-js/core の headless での使い方とイベント処理の例: SDK Demo — Headless
関連ドキュメント
- フロントエンド SDK ガイド — 統合パターンの選択と全体の手順
- React SDK リファレンス —
@asgard-js/reactのコンポーネント