コンテンツへスキップ
CADCAMFUN
ドキュメント

ドキュメント

ストリーミング アシスタント クライアントを構築する

ターンを開始し、実際のイベントをレンダリングし、保留中のリクエストに応答し、接続が切断された後に会話を回復します。

英語から自動翻訳されています。 英語の原文を見る

走行中のサイドカーに接続する

ストリーミング アシスタント クライアントは、デスクトップと同じローカル サイドカーに接続します。割り当てられているループバック URL と現在のアプリケーション ベアラー トークンを、統合している接続から取得します。昨日の起動で保存されたポートは永続的なアドレスではありません。サイドカーは起動するたびに一時的なポートを要求します。アカウント セッション トークン、コンパニオン ペアリング トークン、およびスコープ指定されたアクション ブリッジ トークンには異なる目的があり、このチャット ルート上のアプリケーション ベアラーを置き換えることはできません。

ターンを送信する前にお読みください GET /api/ai/status, /api/ai/providers、およびオプションで /api/ai/models?configuredOnly=true。構成された資格情報は、キーが存在することを意味します。プロバイダーがリクエストを受け入れること、またはエンドポイントが選択したモデルをサポートすることを証明するものではありません。デフォルトのホストは埋め込み nikcli です。ダイレクト モードでは模範解答をストリーミングできますが、埋め込みホストの永続的なツール ループや専門スケジューラは提供されません。

このガイドでは、 CADCAMFUN_URL そして CADCAMFUN_TOKEN クライアントシェル変数として。既存の接続処理を通じてそれらを提供します。を参照してください。 AI API リファレンス 正確なスキーマについては、 サイドカー 起動時の動作について。この例では、資格情報をインストールしたり、ユーザーに代わってモデルを選択したりすることはありません。

プロジェクト、図面、セッション ID を選択します

クライアントは、 sessionId 最初のターンにそれを再利用し、後のターンにそれを再利用します。サービスは、そのクライアント ID を実際のホスト セッションにマップし、各プロンプトの前にバインディングをチェックします。 UI が再接続するたびに異なる識別子を作成するのではなく、イベント/セッション ID を会話状態として保持します。

本物の projectId ターンにそのプロジェクトの許可されたリソースへのアクセスを与えます。オプションの drawingId そのプロジェクトに属している必要があります。それはターンのツールをその図面にバインドします。プロジェクトなしでは図面に名前を付けることはできません。別のアカウントまたはプロジェクトがすでに所有しているセッションは拒否されます。これらは、モデルがプロジェクト ツールを実行できるようになる前の許可チェックであり、システム プロンプトのヒントではありません。

一般的な説明会話の場合は、プロジェクトと図面を省略します。アプリケーションはデフォルトのコンテキストを使用します。ジオメトリ作業の場合、チャット、状態回復、キュー、中止、および制御リクエストを通じて、実際に選択されたプロジェクト/図面 ID を伝えます。アカウントの切り替えでは、以前のアカウントのアクティブなセッションを暗黙的に採用すべきではありません。

ストリーミング ターンを送信する

POST /api/ai/chat/stream sSE本体を返します。ハンドラーが読み取り可能な検証メッセージを返せるようにトランスポート スキーマでいくつかのフィールドがオプションになっている場合でも、空白でないメッセージが必要です。次の一般的な質問では、ストアド コンテキスト インジェクションを無効にし、プロジェクト ジオメトリとは独立してトランスポートをチェックするのに適しています。

最初のストリーミングリクエスト
curl --no-buffer --fail-with-body \
  -H "Authorization: Bearer $CADCAMFUN_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"message":"Explain the difference between G0 and G1.","sessionId":"inspection-demo","includeContext":false,"mode":"gcode"}' \
  "$CADCAMFUN_URL/api/ai/chat/stream"

追加 projectId そして drawingId 特定の図面に取り組むため。 mode 一般、cad、cam、gcode、またはツールパスを受け入れます。あ model セレクターには、プロバイダー、モデル、層、バリアントを含めることができます。層は、高速、バランスの取れた、または最高です。どのベンダーが利用可能かについての仮定をハードコーディングするのではなく、実行中のカタログからプロバイダー/モデル ID を検出します。

permissionMode 承認、質問、または自動を受け入れます。破壊ツール グループは、どのモードでも人間の承認を維持します。の agent フィールドでは、設定されたアシスタントの役割を選択します。読む GET /api/ai/agents ロール ID を推測するのではなく、

完全な SSE データ フレームの読み取り

ルートは Effect の SSE データ モードを使用します。各 SSE データ ペイロードは、 type 差別者。ネットワーク チャンクはイベント境界ではありません。 JSON、行末、UTF-8 文字は読み取り間で分割できます。デコードされたテキストを蓄積し、完全なフレームのみを解析し、複数のデータ行が存在する場合は結合します。

最小限の JSON データ フレーム リーダー
async function* events(response: Response): AsyncGenerator<Record<string, unknown>> {
  if (!response.ok) throw new Error(await response.text())
  if (!response.body) throw new Error("The response has no stream")
  const reader = response.body.getReader()
  const decoder = new TextDecoder()
  let pending = ""
  try {
    while (true) {
      const next = await reader.read()
      pending += decoder.decode(next.value, { stream: !next.done })
      let boundary: RegExpExecArray | null
      while ((boundary = /\r?\n\r?\n/.exec(pending)) !== null) {
        const frame = pending.slice(0, boundary.index)
        pending = pending.slice(boundary.index + boundary[0].length)
        const data = frame.split(/\r?\n/)
          .filter((line) => line.startsWith("data:"))
          .map((line) => line.slice(5).replace(/^ /, "")).join("\n")
        if (!data) continue
        const event: unknown = JSON.parse(data)
        if (!event || typeof event !== "object" || !("type" in event)) {
          throw new Error("Invalid assistant event")
        }
        yield event as Record<string, unknown>
      }
      if (next.done) break
    }
  } finally {
    await reader.cancel().catch(() => {})
    reader.releaseLock()
  }
}

このリーダーはフレーミングを示します。運用クライアントは、共有コントラクトに対して各イベントを検証するか、型指定された生成されたストリーミング クライアントを使用する必要もあります。このエンドポイントのアプリケーション状態は JSON データ レコード内に存在するため、SSE コメント/コントロール フィールドは意図的に無視されます。クライアント ストリームの解放はトランスポートのクリーンアップであり、サーバー側の作業を中止する明示的な要求ではありません。

コンテンツを複製せずに進行状況をレンダリングする

上記と同じベアラーと JSON 本文を使用してフェッチを開始し、リーダーを反復処理します。追加 delta.text 表示される回答テキストとルートへ reasoning.text 独自の見解に。あ done イベントには最終応答テキストが含まれます。回答全体を再度追加するのではなく、蓄積された回答をその最終値で置き換えるか調整します。

イベント維持する状態
turn, session_status, stepビジー/リトライ状態と現在のステップ。ステップ終了の後に、同じターン内で別のステップを続けることができます。
tool_call, tool_progress, tool_result名前、入力、引数の進行状況、出力、成功を含む callId によるレコード。エージェントは、その場にいる場合には専門家を特定します。
tool_diagnostic引数/実行/部分/検証結果、重大度、回復テキスト、およびオプションの突然変異が適用されます。
todo, goal繰り返しコピーを追加するのではなく、現在の計画/目標のスナップショットを置き換えます。
retry, compaction, compacted会話の現在のテキストとセッション ID を維持しながら、継続的な作業について説明します。
heartbeat目に見えるコンテンツはありません。サーバーは、ソース ストリームがアクティブなままである間、約 5 秒ごとに 1 つを送信します。

ツール呼び出しと結果を次のように関連付けます。 callId、配列の位置ではありません。失敗したツールは、アシスタントの続行中に有用な診断を返す可能性があります。編集ツールが成功した後のプロバイダー エラーは、ジオメトリの変更をロールバックしません。

権限と質問に答える

A permission_request sessionId、requestId、権限、パターンが含まれます。リクエストを担当者に提示し、選択した決定を送信します。 POST /api/ai/permission/reply。同等のセッション制御アクションは次のとおりです。 reply_permission。サーバーは、要求が会話または許可された子セッションでまだ保留中であることを確認します。

許可の返信本文
{
  "sessionId": "inspection-demo",
  "requestId": "request-id-from-the-event",
  "decision": "once"
}

決定は一度か、常に、または拒否です。これはサーバー許可の決定であり、confirm などのアクション引数とは異なります。保留中 question_request 順序付けられた質問の配列が含まれます。返信スルー /api/ai/sessions/control アクション付き answer_question、リクエスト ID、および質問ごとに 1 つの回答配列。それを拒否する reject_question.

単一選択の質問に 1 つ答える
{
  "sessionId": "inspection-demo",
  "action": "answer_question",
  "requestId": "question-id-from-the-event",
  "answers": [["The selected option label"]]
}

すべての答えは空であってはなりません。単一選択の質問は複数の回答を拒否し、custom=false はリストされたラベルのみを許可します。正確なラベルと質問の順序を保持します。リクエストが保留中でなくなったとサーバーが判断した場合は、古い応答を繰り返し投稿する代わりに、コントロールを更新します。

作業をキューに入れ、操作し、明示的に停止する

使用 POST /api/ai/chat/queue ターンが機能している間に送信されたメッセージの場合。 sessionId、message、オプションの projectId、およびdeliveryを受け取ります。 queue はデフォルトであり、次の安全なステップを待ちます。 steer ホストに速やかに組み込むよう依頼します。あ user_message イベントは、メッセージが会話に入ったことを通知します。

使用 POST /api/ai/chat/abort ユーザーが停止を押したときの会話 ID を使用します。これによりターンが停止し、関連するワーカーに加わります。フェッチをキャンセルすることのみによって Stop を実装しないでください。HTTP ストリームを失うことは同じ操作ではありません。その後、セッションは、新しいプロジェクト ID を作成せずに別のターンを受け入れることができます。

POST /api/ai/sessions/remove 子セッションをキャンセルして参加した後、ホスト セッションを削除します。 It returns a removed flag.会話を削除しても、そのツールによって行われたプロジェクト編集はロールバックされません。必要に応じて、プロジェクト/履歴 API を通じてそれらを確認または復元します。

ストリームを失った後に回復する

リーダーが最終イベントなしで終了した場合、完了は不明として扱われます。電話 POST /api/ai/sessions/state 同じ sessionId と projectId を使用します。応答にはビジーとテキストが含まれており、オプションで回復されたツール/Todo/診断イベント、使用状況、およびタスクも含まれます。このスナップショットから UI を再構築します。回復されたツール イベントを再生しても、それらのイベントは再度実行されません。

それからリクエストしてください /api/ai/sessions/control アクションステータスを使用して、保留中の権限/質問、ワーカータスク、目標、およびコンテキストポリシーを検査します。未回答の質問を待っている忙しい順番には、元のプロンプトの 2 番目のコピーではなく、返信が必要です。蓄積されたテキストを含むアイドル状態と報告されたセッションは、元の TCP 接続が消滅した場合でも、回復された出力として表示されることがあります。

ネットワーク障害、型指定されたプロバイダー/構成エラー イベント、明示的な中止、およびセッションの削除を区別してください。最初のターンが実際に何をしたかを確認した後でのみ、モデル リクエストを再試行してください。特に、部分的な突然変異の診断または編集作業者の中断の後に、現在の図面を読み取ります。

サーバー内部では、ドロップされたモデル接続が最大 5 回再開され、2 秒のバックオフが発生し、最大で 2 倍の 30 秒になります。ストリームに再接続するクライアントは、セッション状態を読み取る必要があります。これらの再開によって、クライアントがストリームを認識しなかった作業を完了できるためです。

コンテキスト、添付ファイル、および圧縮

includeContext: false 保存されたコンテキストの取得を無効にします。明示的なコンテキスト テキストは、そのターンでも提供できます。通常のアセンブリでは、継続的な要件、現在の観察、UI スナップショット、メモリ、およびサポートする参照を注文し、重複を排除して、24、000 文字の合計予算を適用します。省略または切り捨てられたブロックが報告されます。この予算はプロバイダーのトークン ウィンドウではありません。

添付ファイルには、名前、オプションの MIME、データ、オプションのテキスト/ベース 64 エンコードが含まれます。リクエストごとに最大 5 つあります。画像はそれぞれ 5 MiB、PDF は 10 MiB、テキストは 2、000、000 文字に制限されます。 6、000 文字までの短いテキストもインラインで表示されます。ツール用に保持されるテキストはメモリ内でプロジェクト スコープであるため、再接続はサービスの再起動とは異なります。サポートされていないファイルは理由を付けて拒否されます。

コントロールアクションコンテキストは、詳細なトークンの内訳を要求します。通常のステータスポーリングでは、トークナイザーの作業が回避されます。アクション コンパクトは、ホストに古い会話の内容を要約するよう依頼します。メモリ統合は、別個のホスト操作です。正確なジオメトリを再現するために圧縮された会話に依存するのではなく、独自の API を通じてソース描画および製造リクエストを永続化します。

トランスポートエラーをドメインの結果から分離する

HTTP 401 は、リクエストがベアラー認証を通過しなかったことを意味します。ストリーミング前の 400 は、通常、空のメッセージ、アクセスできないセッション ID、無効なプロジェクト/図面の組み合わせ、または不正な形式の本文を意味します。ストリームが確立されると、ハンドラーは構成、プロバイダー、中止、または内部分類を含むエラー イベントを発行できます。応答ステータスが 200 だった場合でも、そのイベントを処理します。

ok=false のツール結果はツールの結果であり、必ずしも失敗したストリームであるとは限りません。アシスタントや担当者が回復できるように、その出力と診断を表示します。ワーカー キューがいっぱいになることもあります。スケジューラでは、4 つのアクティブなタスクと 16 の待機中のタスクが許可されます。中断されたリーダーは、制限された試行内で回復される可能性があります。編集作業は自動的に再生されるのではなく、不確実なものとして扱われます。

で実行可能な契約を読む packages/api/src/groups/ai.ts そして ai-events.ts;入場と電信行為 apps/sidecar/src/http/ai.ts;のセッションマッピング packages/ai/src/service.ts;での質問の検証 nikcli/session-controls.ts;心拍のタイミング heartbeat.ts。これらの情報源は、アシスタントの散文とは独立して、クライアントがどの結果を提示しなければならないかを定義します。

イベントリファレンス

すべての SSE data: フレームは、 type そして sessionId。完全なフレームのみを解析します。フレームはネットワーク チャンクに分割されて到着する場合があります。

タイプ主要なフィールド取り扱い方法
turnphase: start, recovering, continuing, finish;オプション reason進行状況を表示します。 recovering は、モデルへの接続が再開されていることを意味します。
session_statusstatus: busy, idle, retry送信ボタンとキューを有効または無効にします。
user_message保存されたユーザーメッセージ楽観的な UI を調整します。
deltatext表示されている回答に追加します。
reasoning推論テキストオプション。折りたたみ可能なエリアに表示します。
step, todo, goal計画の進捗状況タスクリストまたは計画をレンダリングします。
tool_callcallId、ツール、引数、オプション agentどのツールがどのスペシャリストに対して実行されるかを表示します。
tool_progresscallId、 進捗長時間実行されているツールを更新します。
tool_resultcallId、 結果結果を示してください。それを一致させる callId.
tool_diagnostic詳細を含む mutationAppliedモデルが変更されたかどうかを判断します。編集を再試行する前に読んでください。
permission_request / permission_resolved / permission_blockedrequestId;返事 once, always, rejectユーザーに尋ねてから、 POST /api/ai/permission/reply.
question_request / question_resolved質問データ質問を提示し、回答を送信します。
context_snapshot, compaction, compactedコンテキスト状態古い履歴が要約されたことをユーザーに通知します。
retryattempt, message一時的な通知を表示します。プロンプトを再送信しないでください。
donetext、オプション usage、オプション recoveredターンの最終テキスト。蓄積されたデルタをそれに置き換えます。
errorcode: provider, config, aborted, internal; messageUI でのメッセージでターンを終了します。 config 通常、資格情報が欠落していることを意味します。
heartbeatなし約 5 秒ごとにキープアライブします。無視する。

完全なストリーミング クライアント

stream-turn.ts
const base = process.env.CADCAMFUN_URL!       // e.g. http://127.0.0.1:53211
const token = process.env.CADCAMFUN_TOKEN!

const response = await fetch(`${base}/api/ai/chat/stream`, {
  method: "POST",
  headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
  body: JSON.stringify({ projectId: "p1", message: "Summarise this project", mode: "general" }),
})
if (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`)

const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()
let buffer = "", answer = ""
for (;;) {
  const { value, done } = await reader.read()
  if (done) break
  buffer += value
  let end
  while ((end = buffer.indexOf("\n\n")) >= 0) {        // one SSE frame
    const frame = buffer.slice(0, end); buffer = buffer.slice(end + 2)
    const data = frame.split("\n").filter((l) => l.startsWith("data:"))
                      .map((l) => l.slice(5).trimStart()).join("\n")
    if (!data) continue
    const event = JSON.parse(data)
    switch (event.type) {
      case "delta": answer += event.text; break
      case "permission_request": /* ask, then POST /api/ai/permission/reply */ break
      case "done": answer = event.text; console.log(answer); break
      case "error": throw new Error(`${event.code}: ${event.message}`)
    }
  }
}

接続が切断された場合は、メッセージを再送信しないでください。セッションを読む POST /api/ai/sessions/stateサーバーがモデル接続を再開し、あなたなしでターンを終了した可能性があるためです。使用 /api/ai/chat/abort 故意にターンを止めること、そして /api/ai/chat/queue セッションがビジー状態のときにメッセージを追加します。

プロバイダーがない場合のストリームはどのように見えるか

AI プロバイダーが構成されていない場合でも、ストリームは開き、入力エラーとそれに続くハートビートで応答します。新しいサイドカーからキャプチャ:

POST /api/ai/チャット/ストリーム
data: {"type":"error","sessionId":"","code":"config","message":"No AI provider is configured. Add an API key in the AI settings."}

data: {"type":"heartbeat"}
  • HTTP ステータスはまだ 200 であり、コンテンツ タイプは text/event-stream: 構成の問題はドメインの結果であり、イベントとして配信されます。
  • sessionId セッションが作成されていないため、空です。
  • heartbeat フレームは接続を維持します。レンダリングではそれらを無視し、進行状況としてカウントしません。
  • 各フレームは空行で終わります。空白行で分割し、結合することで解析します。 data: sDK イテレータと同様に、フレームの行。

モデルを使用せずにクライアントをテストする

認証情報なしでクライアントにサイドカーを指定します。完全な SSE パスとエラー ブランチをコストなしで実行します。

3Dで

会話ストレージ ドラッグして周回します · スクロールしてズーム · 右ドラッグしてパンします
移行による会話、メッセージ、および外部キーを含む関連テーブル。