TypeScript SDK Daemon Client
概要
packages/sdk-typescript/src/daemon/ は TypeScript SDK のデーモンクライアントです。実行中の qwen serve デーモンに接続するための標準的な方法であり、TypeScript / JavaScript ホスト(CLI 自身の TUI アダプター、チャンネルボットバックエンド、VS Code IDE コンパニオン、カスタムスクリプト、サーバーサイド Web バックエンド)から利用されます。他のすべてのアダプターはこれに依存しています。
パッケージのレイアウトは意図的にシンプルにしています:
| ファイル | 公開インターフェース |
|---|---|
index.ts | パブリックバレル(DaemonClient、DaemonSessionClient、DaemonAuthFlow、parseSseStream、イベントリデューサー、型)。 |
DaemonClient.ts | 低レベル HTTP/SSE ファサード — qwen-serve-protocol.md の各ルートに対応するメソッド。 |
DaemonSessionClient.ts | SSE リプレイトラッキングを持つセッションスコープのラッパー。 |
DaemonAuthFlow.ts | 高レベルの OAuth デバイスフローヘルパー。 |
sse.ts | parseSseStream(NDJSON / SSE フレームパーサー)。 |
events.ts | asKnownDaemonEvent、reduceDaemonSessionEvent、reduceDaemonAuthEvent(09-event-schema.md 参照)。 |
types.ts | DaemonCapabilities、DaemonSession、DaemonEvent、PermissionResponse、PromptResult、MCP / エージェント / メモリ / 認証型。 |
ウォークスルーの例は ../examples/daemon-client-quickstart.md にあります。このドキュメントはアーキテクチャとコントラクトのリファレンスです。
責務
- デーモンの各 HTTP ルートに対応する TypeScript メソッドを提供する。
- すべてのリクエストにベアラートークンと
X-Qwen-Client-Idを正しく付与する。 - 長時間の SSE を切断せずに、呼び出し元が指定した
AbortSignalとコール単位のタイムアウトを組み合わせる。 - SSE フレームをストリーミングし、型付き
DaemonEventにパースする。 - セッションごとに
lastSeenEventIdを追跡し、再接続時にリプレイが正しく行われるようにする。 - デーモンが指定した間隔でポーリングするデバイスフロー認証インターフェースを公開する。
アーキテクチャ
DaemonClient (DaemonClient.ts)
コンストラクター:
new DaemonClient({
baseUrl: string, // デフォルト 'http://127.0.0.1:4170'
token?: string,
fetch?: typeof globalThis.fetch, // テスト用にインジェクション可能
fetchTimeoutMs?: number, // 0 = 無効; デフォルト DEFAULT_FETCH_TIMEOUT_MS
});メソッドグループ(すべてのメソッドは X-Qwen-Client-Id を付与するためにオプションの clientId を受け取ります):
| グループ | メソッド |
|---|---|
| プラミング | health()、capabilities()、auth(遅延 DaemonAuthFlow アクセサー) |
| セッション | createOrAttachSession、loadSession、resumeSession、listSessions、closeSession、setSessionMetadata、getSessionContext、getSessionSupportedCommands、setSessionApprovalMode、setSessionModel |
| プロンプト | prompt、cancel、heartbeat |
| イベント | subscribeEvents(SSE ジェネレーター)、subscribeEventsStream(生レスポンス) |
| 権限 | respondToPermission、respondToSessionPermission |
| ワークスペーススナップショット | getWorkspaceMcp、getWorkspaceSkills、getWorkspaceProviders、getWorkspaceEnv、getWorkspacePreflight |
| ワークスペースミューテーション | writeWorkspaceMemory、readWorkspaceMemory、listWorkspaceAgents、getWorkspaceAgent、createWorkspaceAgent、updateWorkspaceAgent、deleteWorkspaceAgent、toggleWorkspaceTool、restartMcpServer、initializeWorkspace |
| ファイル | readFile、readFileBytes、writeFile、editFile、listDirectory、globPaths、statPath |
| 認証 | startDeviceFlow、pollDeviceFlow、cancelDeviceFlow、getAuthStatus |
fetchWithTimeout
すべてのリクエストは fetchWithTimeout を経由します。重要な詳細:
- ボディの読み取りはタイマーのスコープ内です。 以前の実装ではヘッダーが届いた時点でタイマーをクリアしていたため、プロキシがボディの途中で停止した場合、
await res.json()がfetchTimeoutMsを超えてハングする可能性がありました。現在の実装では、ボディ読み取りコードをコールバックとして渡すことで、タイマーがヘッダーの到達とボディの読み取り両方をカバーします。 perCallTimeoutMsは単一のコールでクライアント全体のデフォルトを上書きできます。最も目立つ呼び出し元はrestartMcpServerで、SDK はMCP_RESTART_DEFAULT_TIMEOUT_MS = 330_000(5 分 30 秒)を使用します。デーモン自身のMCP_RESTART_TIMEOUT_MSはちょうど 300 秒ですが、クライアントがその値に合わせると、300 秒付近で完了する再起動がデーモンによる構造化レスポンスのシリアライズと送信の間に競合を引き起こし、誤ったTimeoutErrorが発生する可能性があります。追加の 30 秒はシリアライズ、ネットワーク転送、両端のデコードをカバーします。より短いタイムアウトが必要な呼び出し元はtimeoutMsを渡せます。0を渡すとタイムアウトが無効になります。AbortSignal.anyは呼び出し元が指定したシグナルとコール単位のタイマーシグナルを組み合わせるため、呼び出し元によるキャンセルとコール単位のタイムアウトの両方がきれいにアボートされます。AbortController+ キャンセル可能なsetTimeout(AbortSignal.timeout()の代わり)を使用することで、高速に解決するリクエストがイベントループに保留タイマーをリークしません。タイマーはfinallyでクリアされます。- ストリーミングエンドポイント(
subscribeEvents)はタイムアウトをバイパスします — 長時間の SSE はタイムアウトによって切断されてはなりません。
DaemonSessionClient (DaemonSessionClient.ts)
1 つのセッションにバインドし、lastSeenEventId を自動的に追跡することで、呼び出し元に追加の状態管理を求めることなく SSE リプレイと再接続が機能します。
class DaemonSessionClient {
readonly client: DaemonClient;
readonly session: DaemonSession;
readonly state: DaemonSessionState;
private lastSeenEventId: number | undefined;
static createOrAttach(client, req?): Promise<DaemonSessionClient>;
static load(client, sessionId, req?): Promise<DaemonSessionClient>;
static resume(client, sessionId, req?): Promise<DaemonSessionClient>;
events(opts?: DaemonSessionSubscribeOptions): AsyncIterable<DaemonEvent>;
prompt(req: PromptRequest): Promise<PromptResult>;
cancel(): Promise<void>;
respondToPermission(...): Promise<PermissionResponse>;
setModel(modelServiceId): Promise<SetModelResult>;
heartbeat(): Promise<HeartbeatResult>;
setMetadata(metadata): Promise<SessionMetadataResult>;
close(): Promise<void>;
}events() はデフォルトで resume: true を指定して client.subscribeEvents にプロキシします — 追跡している lastSeenEventId を渡すことで、前のサブスクリプションが停止した場所から再接続時にリプレイが行われます。yield されるイベントごとに lastSeenEventId が更新されます。
DaemonAuthFlow (DaemonAuthFlow.ts)
class DaemonAuthFlow {
start(opts: { providerId, ... }): Promise<DaemonAuthFlowHandle>;
}
interface DaemonAuthFlowHandle {
deviceFlowId: string;
providerId: string;
expiresAt: string;
verificationUrl: string;
userCode: string;
awaitCompletion(opts?): Promise<DaemonAuthDeviceFlowState>;
cancel(): Promise<void>;
}awaitCompletion() は、フローが authorized、failed、または cancelled になるまで、デーモンが指定した intervalMs で GET /workspace/auth/device-flow/:id をポーリングします。client.auth を通じて遅延構築されるため、認証を使用しないクライアントはアロケーションコストを負いません。
parseSseStream (sse.ts)
Response.body(ReadableStream<Uint8Array>)を AsyncIterable<DaemonEvent> に変換します。以下を処理します:
- LF および CRLF フレーミング。
- バッファオーバーフロー上限(16 MiB)— 単一の異常に大きなフレームを出力するデーモンに対する防御的な制限。
- AbortSignal の配線 — アボートするとストリームとイテレーターが閉じられます。
- コメントのみのフレームと未知のイベントタイプ(
DaemonEventとしてそのまま渡され、SDK コンシューマーはasKnownDaemonEventで下流において絞り込みます)。
型 (types.ts)
主なエクスポート:DaemonCapabilities、DaemonSession({ sessionId, workspaceCwd, attached, clientId?, createdAt? })、DaemonEvent、DaemonSessionState、DaemonSessionContextStatus、DaemonSessionSupportedCommandsStatus、PermissionResponse、PromptResult、HeartbeatResult、SetModelResult、SessionMetadataResult、および MCP / エージェント / メモリ / 認証の結果型。
ワークフロー
セッションの作成または接続と最初のプロンプト
リプレイ付きのサブスクライブ
デバイスフロー認証
qwen-oauth はレガシー v1 プロバイダー識別子です。Qwen OAuth 無料ティアは
2026-04-15 に廃止されたため、新しいクライアントは利用可能な現在サポートされている
認証プロバイダーを優先してください。
状態とライフサイクル
DaemonClientはコネクションレスです。コンストラクション時には何も起きません。各メソッドは新しいfetchを開きます。DaemonSessionClientはevents()の呼び出し間でlastSeenEventIdを保持します。再接続時は最後に確認した位置からリプレイされます。DaemonAuthFlowは遅延評価です —client.authが初回アクセス時に構築します。- SSE イテレーターは(a)デーモンがストリームを終了した場合、(b)
AbortSignal.abort()が発火した場合、(c)コンシューマーがfor awaitからブレークアウトした場合、または(d)バッファオーバーフロー上限(16 MiB)に達した場合に閉じられます。
依存関係
globalThis.fetch(Node 18+ 組み込み、ブラウザ、undici など)。テスト用にDaemonClientごとにインジェクション可能。- ネイティブ
AbortController/AbortSignal.any/setTimeout。 @qwen-code/qwen-code-coreや@qwen-code/acp-bridgeへの推移的な依存関係なし — SDK パッケージは完全に分離されており、外部コンシューマーがデーモンの内部実装を引き込みません。
ui/* サブパッケージ(#4328 + #4353 )
SDK は packages/sdk-typescript/src/daemon/ui/ もエクスポートします。これはデーモンイベントをトランスクリプトブロックに変換するホスト中立なプリミティブのセットです:
normalizeDaemonEvent(evt)は 43 の既知のデーモンワイヤーイベントを 37 の UI フレンドリーなDaemonUiEventType値にマッピングします。モデル化されていないまたは不正なイベントはdebugに正規化されます。createDaemonTranscriptState()とreduceDaemonTranscriptEvents(state, events)は UI イベントをDaemonTranscriptBlock[]に投影します。createDaemonTranscriptStore()はサブスクライブ / ディスパッチをラップします。render.ts/terminal.tsは HTML およびターミナルのベースラインレンダラーを提供し、toolPreview.tsはツールコールのサマリーを生成します。- セレクターには
selectTranscriptBlocksOrderedByEventId、selectPendingPermissionBlocks、selectCurrentTool、selectApprovalMode、selectToolProgress、selectSubagentChildBlocks、formatMissedRange、formatBlockTimestampが含まれます。 - パブリック定数には
DAEMON_PLAN_TOOL_CALL_IDが含まれます。 conformance.tsはクロスホスト整合性テストスイートを含みます。
最初のプロダクションコンシューマーは React の DaemonSessionProvider を通じた
packages/webui/src/daemon/ です。詳細なアーキテクチャ、用語集、セレクター一覧、
レガシー DaemonTuiAdapter との関係については 14-cli-tui-adapter.md
を参照してください。
このサブパッケージは @qwen-code/sdk/daemon サブパスからエクスポートされます。
既存の import { DaemonClient } を使用しているコードには影響しません。
設定
| 設定項目 | 場所 | 効果 |
|---|---|---|
baseUrl | DaemonClient コンストラクター | デーモンの URL。末尾のスラッシュは除去されます。 |
token | DaemonClient コンストラクター | Authorization: Bearer として付与されます。 |
fetch | DaemonClient コンストラクター | テスト用のインジェクションポイント。 |
fetchTimeoutMs | DaemonClient コンストラクター | コール単位のタイムアウト。0 = 無効。 |
clientId | メソッドごとのオプション引数 | X-Qwen-Client-Id ヘッダー(08-session-lifecycle.md 参照)。 |
lastEventId | DaemonSessionClient コンストラクター | リプレイカーソルの初期値。 |
maxQueued | サブスクライブごとのオプション | SSE ルートの ?maxQueued=N。事前に caps.features.slow_client_warning を確認してください。 |
perCallTimeoutMs | メソッドごと(例:restartMcpServer) | クライアント全体のタイムアウトを上書きします。 |
注意事項と既知の制限
fetchTimeoutMsはコール単位であり、コネクションレベルではありません。 長いボディの読み取りはタイマーを共有します。レスポンスをストリーミングするデーモンは、コール単位のタイムアウトを上書きするか、タイムアウトを0に設定する必要があります。- SSE はフェッチタイムアウトをバイパスします — 長時間の SSE 接続は
fetchTimeoutMsによって切断されません。呼び出し元によるキャンセルにはAbortSignalを使用してください。 parseSseStreamのバッファ上限は 16 MiB で、防御的な制限です。この上限を超える単一フレームはイテレーターをアボートします(デーモンが正当にそのようなフレームを出力することはありません)。asKnownDaemonEventは未認識のイベントタイプに対してundefinedを返します。 SDK コンシューマーは、ユニオンが網羅的であると仮定するのではなく、このブランチを処理する必要があります。これが前方互換性のコントラクトです。未認識のイベントはDaemonSessionViewState.unrecognizedKnownEventCountをインクリメントします。client_evicted、slow_client_warning、stream_errorはリプレイリングにありません。 退去後に再接続するとデーモンのリングから再開しますが、退去フレームは再び表示されません。DaemonClientは自動リトライを行いません。 ネットワーク障害は拒否として表面化します。再接続 / リプレイの戦略は呼び出し元の責務です(DaemonSessionClient.events()はリプレイを簡単にしますが、再接続はコール単位です)。
リファレンス
packages/sdk-typescript/src/daemon/DaemonClient.tspackages/sdk-typescript/src/daemon/DaemonSessionClient.tspackages/sdk-typescript/src/daemon/DaemonAuthFlow.tspackages/sdk-typescript/src/daemon/sse.tspackages/sdk-typescript/src/daemon/events.tspackages/sdk-typescript/src/daemon/types.ts- エンドツーエンドのウォークスルー:
../examples/daemon-client-quickstart.md。