DeepSeek Harness: 依存関係駆動ロードとホットリロード
最終更新:2026-08-31
「ロード順序を手動制御」から「依存を宣言、フレームに任せる」へ——依存関係駆動ロードは、開発者が関係を宣言するだけで済むことを意味します。Hot Module Replacement(HMR)と組み合わせれば、コード変更が再起動なしで即座に反映。開発体験は劇的に向上します。
📋 前提知識:14-inject.md の完了、inject 依存宣言を理解していること
1. 学習内容
- 依存グラフとトポロジカルソート
- 依存関係準備完了時の自動ロード
- Hot Module Replacement(HMR)機構
- ネストコンテキスト
- 設定変更による再ロード
- 開発時の HMR ベストプラクティス
2. 依存グラフとトポロジカルソート
(1) 依存グラフの構築
起動時、フレームワークはすべてのプラグインの inject 宣言を走査し、有向非巡回グラフ(DAG)を構築します:
function buildDependencyGraph(plugins: Plugin[]) {
const graph = new DAG()
for (const plugin of plugins) {
graph.addNode(plugin.name)
for (const dep of plugin.inject) {
graph.addEdge(dep, plugin.name)
}
}
return graph
}
(2) トポロジカルソート
トポロジカルソートはロード順序を決定します:
プラグイン宣言:
core: inject = []
tools: inject = ['core']
llm: inject = ['core']
my-tool: inject = ['tools', 'llm']
依存グラフ:
core → tools → my-tool
core → llm → my-tool
トポロジカルソート結果:[core, tools, llm, my-tool]
(3) 並列ロード
依存関係のないプラグインは並列にロードできます:
graph TB
subgraph Phase1[フェーズ 1]
CORE[core]
end
subgraph Phase2[フェーズ 2 - 並列]
TOOLS[tools]
LLM[llm]
end
subgraph Phase3[フェーズ 3]
MYTOOL[my-tool]
end
CORE --> TOOLS
CORE --> LLM
TOOLS --> MYTOOL
LLM --> MYTOOL
▶ サンプル 4:
graph TB
CORE[core] --> TOOLS[tools]
CORE --> SESSIONS[sessions]
CORE --> LOGGER[logger]
TOOLS --> MY_TOOL[my-tool]
SESSIONS --> MY_TOOL
LOGGER --> TRAJECTORY[trajectory]
SESSIONS --> TRAJECTORY
3. 依存関係準備完了時の自動ロード
(1) 動的な依存関係の充足
プラグインは起動時にすべての依存関係が満たされている必要はありません。依存サービスが後で登録されると、保留中のプラグインは自動的にアクティブになります:
// プラグイン A:inject = ['tools'] — tools はまだ未登録
// → Fiber 状態:pending
// その後、tools プラグインがロードされサービスを登録
// → プラグイン A の Fiber は自動的に active に遷移し、apply を呼び出し
▶ サンプル 2:
export function apply(ctx: Context) {
if (someCondition) {
ctx.provide('optional-service', impl)
// optional-service に依存する保留中のプラグインが自動的にアクティブに
}
}
▶ サンプル 3:
sequenceDiagram
participant F as Framework
participant A as Plugin A (inject:tools)
participant T as Tools Plugin
F->>A:登録 → pending(tools 未準備)
F->>T:登録 → active
T->>F:tools サービスを登録
F->>A:依存関係準備完了 → active
A->>F:apply() が実行
(4) 永遠に満たされない依存関係
宣言された必須依存が永遠に満たされない場合:
[warn] plugin my-plugin has unsatisfied dependency:nonexistent-service
[warn] my-plugin will remain in pending state
プラグインはエラーにならず——単に永遠に保留状態のまま。オプション依存(? 付き)は未充足でも警告を出しません。
4. Hot Module Replacement(HMR)機構
(1) HMR の概念
ホットモジュール置換は、DSH 全体を再起動せずにランタイムでプラグインコードを置き換えることを可能にします:
graph LR
CHANGE[コード変更] --> DETECT[ファイル変更検出]
DETECT --> DISPOSE[旧 Fiber disposing]
DISPOSE --> LOAD[新コードロード]
LOAD --> ACTIVE[新 Fiber active]
(2) HMR の有効化
pnpm dsh web --patch --watch
--watch フラグはファイル監視を有効化;プラグインソースコードが変更されると自動的にリロードがトリガーされます。
(3) 完全な HMR フロー
- ファイルシステムウォッチャーが
src/index.tsの変更を検出 - 旧プラグインの Fiber が disposing 状態に入る
- すべてのクリーンアップ関数が実行(ctx.effect、自動クリーンアップ)
- 新コードがコンパイル・ロードされる
- 新 Fiber が作成され、pending/active に入る
- 依存プラグインが必要に応じてリロード
(4) HMR の制限
| シナリオ | HMR サポート | 備考 |
|---|---|---|
| execute 関数の変更 | ✅ | ツールロジックがホット更新 |
| Config の変更 | ✅ | 設定が再検証される |
| inject の変更 | ⚠️ | カスケードリロードが発生する可能性 |
| name の変更 | ❌ | 手動再起動が必要 |
| 依存バージョンの変更 | ❌ | 手動再起動が必要 |
(5) カスケードリロード
依存されているプラグインがリロードされると、それに依存するプラグインもリロードされます:
tools プラグイン HMR リロード → my-tool(tools に依存)もリロード
これは依存関係の整合性を保証しますが、「リロードストーム」を引き起こす可能性があります:
core リロード → tools リロード → my-tool リロード → ...(依存チェーン全体がリロード)
5. ネストコンテキスト
(1) コンテキスト階層
Cordis はネストコンテキストをサポート——子コンテキストは親コンテキストのサービスを継承しつつ上書き可能:
export function apply(ctx: Context) {
const childCtx = ctx.extend({
// サービスの上書きまたは追加
})
childCtx.plugin({
name: 'child-plugin',
apply(innerCtx) {
// innerCtx は ctx のサービスを継承
}
})
}
(2) コンテキスト継承ルール
親 ctx:{ tools, llm, sessions }
子 ctx:{ tools(上書き), cache(追加) }
子 ctx から見える:{ tools(上書き版), llm, sessions, cache }
- サービス検索:子 ctx を先にチェック、次に親 ctx(プロトタイプチェーンパターン)
- イベント伝播:子 ctx のイベントは親 ctx にバブルアップ
- リソースクリーンアップ:子 ctx の破棄は親 ctx に影響しない
(3) ネストコンテキストのユースケース
| シナリオ | 説明 |
|---|---|
| セッション分離 | 各セッションが独立した ctx を持つ |
| リクエストスコープ | 各リクエストが一時的な ctx を作成 |
| テスト | 分離されたテストコンテキストを作成 |
| マルチエージェント | 各 Agent が独立したツールセットを持つ |
(4) ネスト深度
理論上は無制限ですが、深すぎるネストはパフォーマンスに影響:
// ❌ 深すぎる
ctx.extend().extend().extend().extend()
// ✅ 適度なネスト
const sessionCtx = ctx.extend({ session })
6. 設定変更による再ロード
(1) 自動再ロード
ユーザーが Web UI でプラグイン設定を変更すると、フレームワークが自動的にリロードをトリガー:
graph LR
UI[Web UI が設定を変更] --> VALID[Schema 検証]
VALID --> OLD[旧 Fiber disposing]
OLD --> NEW[新 Config + 新 Fiber]
NEW --> ACTIVE[Fiber active]
(2) 部分的な設定ホット更新
一部の設定変更はフルリロード不要:
export function apply(ctx: Context) {
ctx.on('config/updated', (newConfig) => {
if (newConfig.debug !== ctx.config.debug) {
ctx.logger.level = newConfig.debug ? 'debug' : 'info'
}
})
}
(3) フルリロードが必要な設定
以下の設定変更は完全なリロードが必要:
- inject リストの変更
- ポート番号の変更
- サービス登録パラメータの変更
(4) 再ロードと永続化
設定変更はリロード後に cordis.yml に永続化:
plugins:
my-plugin:
config:
debug:true # ユーザーが Web UI で変更、自動永続化
7. 開発時の HMR ベストプラクティス
(1) apply を冪等に保つ
apply 関数は冪等であるべき——複数回呼び出しても一貫した結果を生成:
// ✅ 冪等:各 apply は同じツールを登録
export function apply(ctx: Context) {
ctx.tools.register(fileCountTool)
}
// ❌ 非冪等:apply が副作用を蓄積
let counter = 0
export function apply(ctx: Context) {
counter++ // リロード後にカウンターが増加
}
(2) グローバル状態の回避
// ❌ グローバル状態:HMR リロード後も旧状態が残存
const globalCache = new Map()
// ✅ クロージャ状態:各 apply が新しい状態を作成
export function apply(ctx: Context) {
const cache = new Map()
ctx.effect(() => () => cache.clear())
}
(3) クリーンアップ関数の完全性
HMR リロード時、旧 Fiber のクリーンアップ関数はすべてのリソースを完全にクリーンアップする必要があります:
export function apply(ctx: Context) {
const ws = new WebSocket('ws://localhost:8080')
// ✅ クリーンアップを登録
ctx.effect(() => () => ws.close())
// ❌ クリーンアップ忘れ → リロード後に旧接続がリーク
}
(4) 開発ワークフロー
Alice の推奨 HMR 開発ループ:
# 1. HMR 付き開発モードで起動
pnpm dsh web --patch --watch
# 2. コードを普通に書く、保存で自動リロード
# ターミナル出力:
# [hmr] file changed:src/index.ts
# [hmr] disposing my-plugin (old)
# [hmr] loading my-plugin (new)
# [my-plugin] plugin reloaded
# 3. リロードログを確認、クリーンアップエラーがないことを確認
(5) HMR デバッグのコツ
// apply の冒頭にデバッグログを追加
export function apply(ctx: Context) {
ctx.logger.info('apply called at', new Date().toISOString())
// ...
}
// apply が予期せず複数回呼び出される場合、HMR カスケードリロードが発生している
❓ よくある質問
[hmr] loading xxx (new) と [xxx] plugin reloaded を出力。失敗はエラーメッセージを出力。--watch フラグは開発専用です。📖 まとめ
- 依存グラフは inject 宣言から DAG として構築;トポロジカルソートがロード順序を決定
- 動的な依存充足:保留中のプラグインは依存関係が準備でき次第自動的にアクティブ化
- HMR は
--watchで有効化;コード変更でプラグインが自動リロード - ネストコンテキストは親サービスを継承し、上書きと分離をサポート
- 設定変更は自動リロードをトリガー;一部の変更はホット更新可能
- HMR ベストプラクティス:冪等な apply、グローバル状態の回避、完全なクリーンアップ関数
📝 練習問題
1. ⭐ 基礎:pnpm dsh web --patch --watch を起動し、ロード済みプラグインの apply 関数を修正(ログ行を追加)し、保存後のターミナルで HMR リロードメッセージを観察してください。
2. ⭐⭐ 応用:依存関係のある2つのプラグイン(A inject B)を作成してください。HMR を起動し、B のコードを変更して A がカスケード的にリロードされるか観察。次に A のコードのみを変更し、B が影響を受けないことを確認。
3. ⭐⭐⭐ チャレンジ:グローバル変数を使用するプラグイン(非冪等)を書いてください。HMR リロード後の変数値の変化を観察。その後クロージャ状態にリファクタリング(冪等)し、リロード後の一貫した動作を確認。リファクタリング前後のターミナル出力を比較して記録すること。