DeepSeek Harness: 依存関係駆動ロードとホットリロード

最終更新:2026-08-31

「ロード順序を手動制御」から「依存を宣言、フレームに任せる」へ——依存関係駆動ロードは、開発者が関係を宣言するだけで済むことを意味します。Hot Module Replacement(HMR)と組み合わせれば、コード変更が再起動なしで即座に反映。開発体験は劇的に向上します。

💡 ヒント:依存関係駆動 + HMR = 「変更すれば即反映」。依存関係は宣言するだけ、ロード順序はフレームワークが処理。コードは保存するだけ、ホットリロードはフレームワークが処理。核心的な生産性向上は「再起動待ち」の時間を排除することです。

📋 前提知識14-inject.md の完了、inject 依存宣言を理解していること

1. 学習内容

依存駆動ロードとリロード


2. 依存グラフとトポロジカルソート

(1) 依存グラフの構築

起動時、フレームワークはすべてのプラグインの inject 宣言を走査し、有向非巡回グラフ(DAG)を構築します:

TYPESCRIPT
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) トポロジカルソート

トポロジカルソートはロード順序を決定します:

TEXT 📖 参照専用
プラグイン宣言:
  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) 並列ロード

依存関係のないプラグインは並列にロードできます:

100%
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:

100%
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) 動的な依存関係の充足

プラグインは起動時にすべての依存関係が満たされている必要はありません。依存サービスが後で登録されると、保留中のプラグインは自動的にアクティブになります:

TYPESCRIPT
// プラグイン A:inject = ['tools'] — tools はまだ未登録
// → Fiber 状態:pending

// その後、tools プラグインがロードされサービスを登録
// → プラグイン A の Fiber は自動的に active に遷移し、apply を呼び出し

▶ サンプル 2:

TYPESCRIPT
export function apply(ctx: Context) {
  if (someCondition) {
    ctx.provide('optional-service', impl)
    // optional-service に依存する保留中のプラグインが自動的にアクティブに
  }
}

▶ サンプル 3:

100%
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) 永遠に満たされない依存関係

宣言された必須依存が永遠に満たされない場合:

TEXT 📖 参照専用
[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 全体を再起動せずにランタイムでプラグインコードを置き換えることを可能にします:

100%
graph LR
    CHANGE[コード変更] --> DETECT[ファイル変更検出]
    DETECT --> DISPOSE[旧 Fiber disposing]
    DISPOSE --> LOAD[新コードロード]
    LOAD --> ACTIVE[新 Fiber active]

(2) HMR の有効化

BASH
pnpm dsh web --patch --watch

--watch フラグはファイル監視を有効化;プラグインソースコードが変更されると自動的にリロードがトリガーされます。

(3) 完全な HMR フロー

  1. ファイルシステムウォッチャーが src/index.ts の変更を検出
  2. 旧プラグインの Fiber が disposing 状態に入る
  3. すべてのクリーンアップ関数が実行(ctx.effect、自動クリーンアップ)
  4. 新コードがコンパイル・ロードされる
  5. 新 Fiber が作成され、pending/active に入る
  6. 依存プラグインが必要に応じてリロード

(4) HMR の制限

シナリオ HMR サポート 備考
execute 関数の変更 ツールロジックがホット更新
Config の変更 設定が再検証される
inject の変更 ⚠️ カスケードリロードが発生する可能性
name の変更 手動再起動が必要
依存バージョンの変更 手動再起動が必要

(5) カスケードリロード

依存されているプラグインがリロードされると、それに依存するプラグインもリロードされます:

TEXT 📖 参照専用
tools プラグイン HMR リロード → my-tool(tools に依存)もリロード

これは依存関係の整合性を保証しますが、「リロードストーム」を引き起こす可能性があります:

TEXT 📖 参照専用
core リロード → tools リロード → my-tool リロード → ...(依存チェーン全体がリロード)

5. ネストコンテキスト

(1) コンテキスト階層

Cordis はネストコンテキストをサポート——子コンテキストは親コンテキストのサービスを継承しつつ上書き可能:

TYPESCRIPT
export function apply(ctx: Context) {
  const childCtx = ctx.extend({
    // サービスの上書きまたは追加
  })
  
  childCtx.plugin({
    name: 'child-plugin',
    apply(innerCtx) {
      // innerCtx は ctx のサービスを継承
    }
  })
}

(2) コンテキスト継承ルール

TEXT 📖 参照専用
親 ctx:{ tools, llm, sessions }
子 ctx:{ tools(上書き), cache(追加) }

子 ctx から見える:{ tools(上書き版), llm, sessions, cache }

(3) ネストコンテキストのユースケース

シナリオ 説明
セッション分離 各セッションが独立した ctx を持つ
リクエストスコープ 各リクエストが一時的な ctx を作成
テスト 分離されたテストコンテキストを作成
マルチエージェント 各 Agent が独立したツールセットを持つ

(4) ネスト深度

理論上は無制限ですが、深すぎるネストはパフォーマンスに影響:

TYPESCRIPT
// ❌ 深すぎる
ctx.extend().extend().extend().extend()

// ✅ 適度なネスト
const sessionCtx = ctx.extend({ session })

6. 設定変更による再ロード

(1) 自動再ロード

ユーザーが Web UI でプラグイン設定を変更すると、フレームワークが自動的にリロードをトリガー:

100%
graph LR
    UI[Web UI が設定を変更] --> VALID[Schema 検証]
    VALID --> OLD[旧 Fiber disposing]
    OLD --> NEW[新 Config + 新 Fiber]
    NEW --> ACTIVE[Fiber active]

(2) 部分的な設定ホット更新

一部の設定変更はフルリロード不要:

TYPESCRIPT
export function apply(ctx: Context) {
  ctx.on('config/updated', (newConfig) => {
    if (newConfig.debug !== ctx.config.debug) {
      ctx.logger.level = newConfig.debug ? 'debug' : 'info'
    }
  })
}

(3) フルリロードが必要な設定

以下の設定変更は完全なリロードが必要:

(4) 再ロードと永続化

設定変更はリロード後に cordis.yml に永続化:

YAML
plugins:
  my-plugin:
    config:
      debug:true  # ユーザーが Web UI で変更、自動永続化

7. 開発時の HMR ベストプラクティス

(1) apply を冪等に保つ

apply 関数は冪等であるべき——複数回呼び出しても一貫した結果を生成:

TYPESCRIPT
// ✅ 冪等:各 apply は同じツールを登録
export function apply(ctx: Context) {
  ctx.tools.register(fileCountTool)
}

// ❌ 非冪等:apply が副作用を蓄積
let counter = 0
export function apply(ctx: Context) {
  counter++  // リロード後にカウンターが増加
}

(2) グローバル状態の回避

TYPESCRIPT
// ❌ グローバル状態:HMR リロード後も旧状態が残存
const globalCache = new Map()

// ✅ クロージャ状態:各 apply が新しい状態を作成
export function apply(ctx: Context) {
  const cache = new Map()
  ctx.effect(() => () => cache.clear())
}

(3) クリーンアップ関数の完全性

HMR リロード時、旧 Fiber のクリーンアップ関数はすべてのリソースを完全にクリーンアップする必要があります:

TYPESCRIPT
export function apply(ctx: Context) {
  const ws = new WebSocket('ws://localhost:8080')
  
  // ✅ クリーンアップを登録
  ctx.effect(() => () => ws.close())
  
  // ❌ クリーンアップ忘れ → リロード後に旧接続がリーク
}

(4) 開発ワークフロー

Alice の推奨 HMR 開発ループ:

BASH
# 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 デバッグのコツ

TYPESCRIPT
// apply の冒頭にデバッグログを追加
export function apply(ctx: Context) {
  ctx.logger.info('apply called at', new Date().toISOString())
  // ...
}
// apply が予期せず複数回呼び出される場合、HMR カスケードリロードが発生している

❓ よくある質問

Q HMR と手動再起動の違いは?
A HMR は変更されたプラグインとその依存チェーンのみリロード;他のプラグインは影響を受けません。手動再起動はすべてのプラグインをリロードするため時間がかかります。
Q HMR リロード中にセッションは失われますか?
A いいえ。セッションデータは sessions サービスが管理し、HMR の影響を受けません。失われるのはリロードされたプラグインの状態のみです。
Q リロードが成功したかどうかを確認するには?
A ターミナルログを確認。成功リロードは [hmr] loading xxx (new)[xxx] plugin reloaded を出力。失敗はエラーメッセージを出力。
Q カスケードリロードが頻繁すぎる場合は?
A 不要な低レベルプラグイン(core 等)への依存がないか確認。ツールプラグインが tools にのみ依存していれば、core がリロードされてもカスケードしません。
Q ネストコンテキスト内の HMR の動作は?
A 子コンテキスト内のプラグインがリロードされた場合、その子コンテキスト内のプラグインのみ影響を受け、親コンテキストは影響を受けません。
Q プロダクションで HMR を使うべきですか?
A いいえ。HMR は開発ツール;プロダクションでは安定したロードを使用すべきです。--watch フラグは開発専用です。

📖 まとめ


📝 練習問題

1. ⭐ 基礎pnpm dsh web --patch --watch を起動し、ロード済みプラグインの apply 関数を修正(ログ行を追加)し、保存後のターミナルで HMR リロードメッセージを観察してください。

2. ⭐⭐ 応用:依存関係のある2つのプラグイン(A inject B)を作成してください。HMR を起動し、B のコードを変更して A がカスケード的にリロードされるか観察。次に A のコードのみを変更し、B が影響を受けないことを確認。

3. ⭐⭐⭐ チャレンジ:グローバル変数を使用するプラグイン(非冪等)を書いてください。HMR リロード後の変数値の変化を観察。その後クロージャ状態にリファクタリング(冪等)し、リロード後の一貫した動作を確認。リファクタリング前後のターミナル出力を比較して記録すること。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%