アプリを作る
組み込みチャット
会話を Hane に保存し、メッセージごとに Agent を選んで頼み、進行を SSE で受け取る組み込みチャットの作り方です。
このページでは、アプリに「Agent に頼む」チャットを組み込む方法を説明します。会話とメッセージは Hane に保存され、利用者の文はチャットの Agent が受けて、必要な業務の Agent を呼びます。外部のチャットサービスの席の契約は要りません。
- 利用者が会話にメッセージを送ると、Hane はメッセージを保存し、同じトランザクションでチャットの Agent(定義で
default: trueの Agent)の AgentRun を始めます。 - チャットの Agent は、
agents.listで呼べる業務の Agent を確かめ、agents.runで呼んで結果を受け取り、答えを返事にします。1 つの頼みで複数の業務の Agent を呼ぶこともあります。 - 足りない入力があれば、同じ会話で聞き返します。
- 応答の生成、ツールの開始と終了、承認待ちなどの変化は、会話のイベント(SSE)で届きます。再接続すると、履歴と現在の状態から続きを受け取れます。
外への操作は、AgentRun と同じく承認の Task に回ります。チャットの返事を承認として扱いません。
チャットの Agent を定義する
Section titled “チャットの Agent を定義する”有効なバージョンに、チャットの Agent を 1 つだけ置きます。ツールは agents.list と agents.run だけで、呼んでよい業務の Agent を agents に並べます。
apiVersion: agents/v1alpha1kind: Agentmetadata: name: assistantspec: purpose: 頼みに合う業務の Agent を呼んで、その結果を答える default: true input: type: object required: [request] properties: request: { type: string, title: 頼まれた文, format: message } documents: { type: string, title: 添付した文書, format: document } driver: llm instructions: |- 自分では業務をせず、agents.list で呼べる Agent を確かめ、agents.run で呼んで、結果を短く答える。 必要な値が文にも会話にもなければ、呼ばずに聞き返す。 tools: [agents.list, agents.run] agents: [lease-qa, quote-request, vehicle-check] output: type: object required: [status, answer] properties: status: oneOf: - { const: answered, title: 答えた } - { const: needs_input, title: 足りない値を聞いた } - { const: cannot, title: 頼めなかった } answer: { type: string, maxLength: 2000 }チャットの Agent はモデルのドライバー(llm・codex・claude)で動かします。チャットの Agent がない有効なバージョンでは、チャットは動きません。
API で会話する
Section titled “API で会話する”-
会話を作る
const conv = await unwrap(hane.POST("/v1/agents/conversations", { body: {} }));会話は作った利用者だけが読めます。題名を渡さなければ、最初のメッセージの 1 行目が題名になります。
-
メッセージを送る
const msg = await unwrap(hane.POST("/v1/agents/conversations/{id}/messages", {params: { path: { id: conv.conversation_id } },body: {content: "C-1001 の満了日を教えて",client_message_id: crypto.randomUUID(), // 送り直しでも同じ値を使うagent: "lease-qa", // 任意。このメッセージではこの Agent だけを呼ぶ},}),);// 202。msg.run_id はチャットの Agent の実行agentを省くと、チャットの Agent が文から業務の Agent を選びます。- チャットの Agent が呼べない Agent を指定すると
VALIDATION_FAILEDになり、何も保存しません。 - 返事が終わるまで、同じ会話の次のメッセージは
CONFLICTになります。 - 先にアップロードした文書を
attachments: [{ document_id }]で 5 つまで添付できます。
-
イベントを受け取る
const res = await hane.GET("/v1/agents/conversations/{id}/events", {params: { path: { id: conv.conversation_id }, ...(lastId ? { header: { "Last-Event-ID": lastId } } : {}) },parseAs: "stream",});// res.data は ReadableStream。再接続では最後に受け取った id を Last-Event-ID に渡す
イベント(SSE)
Section titled “イベント(SSE)”イベントは text/event-stream で届きます。
| event | data |
|---|---|
snapshot |
会話の現在状態。conversation・items(メッセージ)・turns(実行とその ActionRequest)・activities(進行記録)・cursor |
failure |
接続中の拒否(status・code・message)。接続は閉じる |
- 最初に現在状態の
snapshotが届き、その後は変化があるたびに届きます。SSE のidはcursorです。 activitiesは、Last-Event-IDより後の進行記録です。再接続しても同じ記録を二重に受け取りません。- 15 秒ごとにコメントの heartbeat が届き、45 秒以内に接続が閉じます。再接続のたびに、認証と閲覧の権限を確かめ直します。
- 推論の内部の過程、資格情報、モデルの内部の JSON は届きません。
進行記録の kind は次のとおりです。message.accepted・agent.queued・agent.started・tool.started・tool.completed・tool.failed・approval.pending・action.updated・agent.completed。
BFF で中継する
Section titled “BFF で中継する”ブラウザは Hane を直接呼ばないので、BFF がイベントの接続を中継します。参照アプリは、ブラウザの Last-Event-ID を Hane に渡し、ブラウザが切断したら上流の接続も止めています。中継の応答には content-type: text/event-stream・cache-control: no-cache, no-transform を付け、途中で溜めないようにします。
UI の部品を使う
Section titled “UI の部品を使う”UI ライブラリ @hane/ui の AgentChat は、会話の一覧、入力欄(/ で Agent を指定、添付、送信の取り消し)、手順の表示、承認と結果のカードを持つ部品です。アプリのサーバーの呼び方は ChatTransport で渡します。次の例の bff は、BFF の /bff/* を呼ぶアプリの関数です。
import { AgentChat, type ChatTransport, subscribeEventSource } from "@hane/ui";
const transport: ChatTransport = { createConversation: () => bff("/agents/conversations", { method: "POST", body: {} }), sendMessage: (id, body) => bff(`/agents/conversations/${id}/messages`, { method: "POST", body }), subscribe: (id, listener) => subscribeEventSource(`/bff/agents/conversations/${id}/events`, listener), cancelRun: (id) => bff(`/agents/runs/${id}/cancel`, { method: "POST" }),};
<AgentChat agents={agents} conversations={conversations} conversationId={id} onOpen={open} transport={transport} />;transport は描くたびに作り直さず、同じものを渡します(作り直すと接続し直します)。approvalTasks・decideApproval を渡すと、自分が決められる承認をカードで決められます。決定は Task の完了として記録されます。