0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【#8】 OpenClaw を読み解く — 会話と文脈を育て組み立てる規律

0
Last updated at Posted at 2026-06-27

【#8】 OpenClaw を読み解く — 会話と文脈を育て組み立てる規律

本記事のコード参照は OpenClaw maincee2aca409(version 2026.6.10)時点。行番号は更新でズレ得ます。

#07 のエージェントループは「文脈(context)」を受け取って LLM を呼びます。その文脈を作り、保存し、溢れたら圧縮するのが今回のテーマ。会話履歴を木構造で持つセッションと、トークン予算内にプロンプトを組み立てるコンテキストエンジンsrc/agents/sessions/src/context-engine/src/trajectory/ から読みます。

会話は木である — parentId が結ぶ枝と分岐

OpenClaw のセッションは、追記専用(append-only)の JSONL ツリーです。各エントリは次を持ちます。

  • id: 8 文字の一意 ID(衝突チェックつき)
  • parentId: 親エントリへのポインタ(これで DAG を成す)
  • type: message / compaction / branch_summary / custom など
  • timestamp: ISO 文字列

保存先は ~/.openclaw/agent/sessions/--{cwd-encoded}--/{sessionId}.jsonl。1行=1 JSON で、書き込みはファイル単位でキューイングされ順序が保証されます(src/config/sessions/transcript-append.ts:37)。

なぜ単純な配列ではなくなのか。分岐(branching)を表現するためです。leafId ポインタが「いまどこにいるか」を指し、過去のエントリに leaf を戻せば、そこから別の枝を伸ばせます。

文脈の組み立ては leaf から root への遡上

LLM に渡す履歴は、現在の leaf から parentId を辿って root まで遡り、反転して得ます(buildSessionContext(), src/agents/sessions/session-manager.ts:383)。

const path: SessionEntry[] = [];
let current: SessionEntry | undefined = leaf;
while (current) {
  path.unshift(current);
  current = current.parentId ? byId.get(current.parentId) : undefined;
}
return buildCoreSessionContext(path as CoreSessionTreeEntry[]) as SessionContext;

byId マップで O(1) ルックアップ。アクティブな枝に乗っているエントリだけが文脈に入るので、別の枝(過去に分岐して捨てた会話)は混ざりません。

追記は必ず appendMessage を通す

#03 で予告した invariant がここです。メッセージ追記は appendMessage()session-manager.ts:2333)を必ず通します。

appendMessage(message): string {
  const entry: SessionMessageEntry = {
    type: "message",
    id: generateId(this.byId),
    parentId: this.appendParentId, // 追記位置(論理親を加味した leaf)を親に
    timestamp: new Date().toISOString(),
    message,
  };
  this.appendEntry(entry);       // JSONL に追記し leaf を前進
  return entry.id;
}

parentId に渡すのは素の leafId ではなく appendParentId です。通常はこれが現在の leaf を指しますが、分岐や論理親(logicalParentsById)が絡む場面では「いま追記すべき親」を解決した値になります。

src/gateway/server-methods/AGENTS.md が「生 JSONL で type: "message" を直書きするな、必ず SessionManager.appendMessage(...) を使え」と強制するのはこのためです。直書きは parentId チェーンを壊し、圧縮・履歴を破壊します。

差し替えられる組み立て器 — コンテキストエンジンの契約

文脈の組み立てロジック自体がプラグイン化されています。ContextEngine インターフェイス(src/context-engine/types.ts:298)はこんな契約です。

  • bootstrap(): セッション単位の初期化
  • ingest(): メッセージ1件の取り込み
  • assemble(): トークン予算内で、モデルに渡す順序付きメッセージを組み立てる
  • compact(): 要約・刈り込みでトークンを削る
  • afterTurn() / maintain(): ターン後の保守・トランスクリプト書き換え

assemble() の返り値が示唆に富みます。

type AssembleResult = {
  messages: AgentMessage[];
  estimatedTokens: number;
  promptAuthority?: "assembled" | "preassembly_may_overflow";
  systemPromptAddition?: string;
  contextProjection?: ContextEngineProjection;
};

promptAuthority で「この組み立てが権威ある最終形か、それとも事前見積もりで溢れるかも知れないか」を返す。トークン予算はモデルのコンテキストウィンドウから予約分を引いて算出し、送信前に圧縮要否を判定します。

溢れたら畳む — /compact は過去を壊さない

/compact のCLI側ライフサイクルは src/agents/command/cli-compaction.ts(入口は runCliTurnCompactionLifecycle)が束ねます。ただし実処理は複数モジュールに分散しており、おおよそ次の流れです。

  1. 圧縮要否の判定 — shouldCompact は関数ではなく真偽値フィールドで、src/agents/embedded-agent-runner/run/preemptive-compaction.ts が算出する
  2. prepareCompaction()src/agents/sessions/compaction/compaction.ts)で履歴の切断点を特定
  3. generateSummary()(圧縮ランナー側の依存)で刈り込む部分を LLM で要約
  4. CompactionEntry を要約+ firstKeptEntryId 付きでトランスクリプトに追記

これらは cli-compaction.ts が一直線にこの順で呼ぶわけではなく、cli-compaction.ts がオーケストレーションし、判定・準備・要約は上記の各モジュールが担う、という分担です。

CompactionEntry の形(session-manager.ts:95)はこうです。

interface CompactionEntry<T = unknown> extends SessionEntryBase {
  type: "compaction";
  summary: string;
  firstKeptEntryId: string;   // ここから先は原文を保持
  tokensBefore: number;
  details?: T;
  fromHook?: boolean;
}

圧縮も木に追記するだけで、過去を破壊しません。「firstKeptEntryId 以前は要約で代替、以降は原文」という形で、文脈組み立て時に古い部分を要約に差し替えられます。

セッションコマンドの他の2つ: /new は TUI スコープの一意セッションキー(tui-{uuid})を作ってクライアント状態を隔離、/resetresetSession() でランタイムメタデータをクリア(履歴は非破壊で温存)します。

もう一つの流れ — 実行を刻むトラジェクトリ

セッションが「会話の木」なら、トラジェクトリ(src/trajectory/)は「実行イベントの流れ」です。TrajectoryEventtypes.ts:13)はバージョン付き封筒で、

  • source: "runtime" | "transcript" | "export"
  • type: イベント種別(tool-call, model-complete など)
  • sessionId / runId / entryId / parentEntryId: 実行・セッションとの対応
  • data: イベント固有ペイロード
  • ts / seq / sourceSeq: タイミングと順序

セッショントランスクリプトを補完する位置づけで、同じ sessionId で相互参照します。エクスポート時には TrajectoryBundleManifest を生成し、ランタイムイベントとトランスクリプトエントリを結びつけます(冒頭の git ログにあった "trajectory session branch bundle" のテストはこの周辺です)。

並び順という静かな規律 — キャッシュを壊さないために

地味ですが効くのが、ルート AGENTS.md のこの規約です。

Prompt cache: deterministic ordering for maps/sets/registries/plugin lists/files/network results before model/tool payloads. Preserve old transcript bytes when possible.

LLM プロバイダのプロンプトキャッシュは「前回と同じ先頭バイト列」のときに効きます。マップやセット、プラグイン一覧の順序がリクエストごとに揺れると、キャッシュが無効化されて遅く・高くなる。だからモデル/ツールペイロードに載せる前に決定論的に並べる。実装例が src/agents/prompt-cache-stability.ts:17 です。

export function normalizePromptCapabilityIds(capabilities: ReadonlyArray<string>): string[] {
  const seen = new Set<string>();
  const normalized: string[] = [];
  for (const capability of capabilities) {
    const value = normalizeLowercaseStringOrEmpty(normalizeStructuredPromptSection(capability));
    if (!value || seen.has(value)) continue;
    seen.add(value);
    normalized.push(value);
  }
  return normalized.toSorted((left, right) => left.localeCompare(right));
}

重複排除し、空白・改行を正規化し、localeCompare で辞書順ソート。ツールも名前順に materialize され、入力順 [zeta, alpha, mu] でも [alpha, mu, zeta] で並ぶことがテストで担保されています。「キャッシュを壊さない」という運用要件が、コードの順序規律として結晶しているわけです。

まとめ — 木に追記し、順序で速さを守る

  • セッションは parentId で繋がる JSONL の木。分岐を表現でき、追記は必ず appendMessage 経由。
  • 文脈は leaf から root への遡上で組み立て、アクティブな枝だけを使う。
  • コンテキストエンジンは差し替え可能で、assemble がトークン予算内の最終形を返す。
  • 圧縮も木への追記CompactionEntry)で、過去を破壊しない。
  • トラジェクトリは実行イベントの記録で、セッションを補完。
  • 決定論的順序でプロンプトキャッシュを効かせる規律が、コードに浸透している。

次回予告 — 会話を越える記憶へ

#09 は、会話を越えて記憶を持つ仕組み、メモリシステムです。「同時に有効化できるメモリプラグインは1つだけ」という排他スロット、MemorySearchManager 契約、そして memory-core / lancedb / wiki / active-memory の役割分担を読み解きます。

図1.png

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?