Skip to content

本文はリポジトリ docs/core-integration.md から同期しています。文中のソースコードへのリンクは GitHub リポジトリへ遷移し、 サイト内で読める関連ページはサイト内リンクに変更しています。

@zqagent/core をサードパーティシステムに統合する:完全統合マニュアル ​

簡体字中国語 · 現行リポジトリ 0.3.25 に対応

本文は、AI Agent 機能を自社システムに組み込もうとするチーム向けです。あなたのシステムは Web サービス、 デスクトップアプリ、タスクキューの Worker、あるいはすでにセッション/ユーザー/課金体系を持つプロダクトかもしれません。 目標は CLI をインストールせず、zqagent の server も起動せず、@zqagent/core をライブラリとして扱い、 「モデル + ツール + 権限 + イベントストリーム + 永続化」という Agent 機能一式を手に入れることです。

まず最小例だけを動かしたい場合:embedding.md と examples/embed-basic を参照してください。 本文はその完全版であり、すべての公開 API、統合モード、本番運用上の注意点を扱います。


目次 ​

  1. それが何であり、何ではないか
  2. アーキテクチャ上の位置づけとパッケージ関係
  3. インストールと実行環境
  4. 5分で動かす
  5. 中核概念
  6. エンジンの組み立て
  7. ツール:組み込みとカスタム
  8. 権限、承認、セキュリティ境界
  9. イベントストリーム完全リファレンス
  10. モデル Provider
  11. ライフサイクルフックと永続化
  12. 1ターン Turn の制御
  13. 添付ファイル
  14. コンテキスト圧縮
  15. MCP 統合
  16. サブエージェントと Agent レジストリ
  17. 変更追跡とロールバック
  18. 4つの統合モードとコード骨子
  19. 本番チェックリスト
  20. API クイックリファレンス
  21. トラブルシューティング FAQ
  22. バージョン、ビルド、リリース

1. それが何であり、何ではないか ​

@zqagent/core は Agent エンジンライブラリです。担当するのは:

  • 1つのセッションのメッセージ履歴を維持する;
  • LLM(ストリーミング)を呼び出し、テキスト、推論過程、ツール呼び出しを解析する;
  • ToolRegistry を通じてツールを実行する;
  • PermissionEngine を通じて、各ツール呼び出しを許可・拒否・人間への確認のどれにするか判断する;
  • 非同期ジェネレータのイベントストリームとして、全過程を呼び出し側に公開する;
  • コンテキストが上限を超えた場合に履歴を圧縮する;
  • オプション:イベントとチェックポイントの永続化、MCP 統合、サブエージェント、Git スナップショット。

担当しないのは:

  • UI / ターミナル描画(ConsoleHook は参考実装にすぎない);
  • HTTP / WebSocket / SSE サービス(@zqagent/server は参考実装);
  • ユーザー、テナント、課金、認証;
  • セッションインデックスとデータベース(FileLogHook はファイルレベルの参考実装);
  • 呼び出し側の代わりに承認判断を行うこと。

一言で言えば:あなたがプロダクトの外殻とセッションのライフサイクルを担当し、エンジンが1ターンの会話をどのように実行するかを担当します。


2. アーキテクチャ上の位置づけとパッケージ関係 ​

                   ┌─────────────────────────────┐
   你的系统 ──────▶│ @zqagent/core               │
   (Web/桌面/Worker)│ TurnEngine / ToolRegistry   │
                   │ PermissionEngine / Provider │
                   │ 内置工具 / MCP / 压缩 / VCS  │
                   └──────────────┬──────────────┘
                                  │ 依赖
                                  ▼
                   ┌─────────────────────────────┐
                   │ @zqagent/shared             │
                   │ 模型能力表 / 协议 / 加密等   │
                   └─────────────────────────────┘

   仓库自带的两个「完整应用」,可作为参考实现:
   @zqagent/server = 多租户会话 + HTTP/SSE + 挂起式审批
   @zqagent/cli    = 终端渲染 + 会话持久化 + 交互审批

依存方向は一方向です:shared ← core。core は server / cli の存在を知らず、 逆方向に依存することもありません。サードパーティシステムが core を導入する際に、HTTP サービス、Tauri、 コンソール UI などを一緒に引き込むことはありません。

リポジトリ内のパッケージ:

パッケージ用途単独利用可能か
@zqagent/coreAgent エンジン(本文の主役)はい
@zqagent/sharedcore と hub が共有する基礎モジュールcore の依存として自動インストール
@zqagent/serverマルチテナント HTTP サービスの参考実装はい。ただしアプリケーション層に属する
@zqagent/cliターミナルアプリの参考実装はい。ただしアプリケーション層に属する

3. インストールと実行環境 ​

3.1 環境要件 ​

項目要件
Node.js>= 18.17(package.json の engines)
モジュールシステムESM と CommonJS のデュアル成果物。import / require のどちらも利用可能
型型宣言はパッケージに同梱(dist/index.d.ts / .d.cts)
OSLinux / macOS / Windows。shell ツールは Windows で利用可能な shell を自動選択

3.2 インストール ​

bash
npm i @zqagent/core
# 或
pnpm add @zqagent/core
# 或
yarn add @zqagent/core

バージョン確認:本文はリポジトリ 0.3.25 に対応しています。npm 上の利用可能バージョンは npm view @zqagent/core version を基準にしてください。npm のバージョンがリポジトリより遅れている場合は、 リポジトリ内で pnpm --filter @zqagent/core build した後、file: / workspace 参照で利用できます。 package.json で正確なバージョンを固定し、エンジンの挙動がマイナーバージョンでずれるのを避けることを推奨します。

3.3 TypeScript 設定 ​

パッケージは node16 モジュール解決を使用し、types 条件付きでエクスポートします。利用側の推奨設定:

jsonc
{
  "compilerOptions": {
    "target": "es2022",
    "module": "node16",
    "moduleResolution": "node16",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

プロジェクトが純粋な ESM("type": "module")なら直接 import します。CommonJS の場合も require("@zqagent/core") は利用できますが、0.2.60 以上のバージョンが必要です(それ以前の CJS 成果物は ESM-only 依存関係により読み込みに失敗します)。

3.4 サブパスインポート ​

packages/core/package.json の exports は閉じられており、以下のサブパスのみ利用できます。 ツール実装を必要に応じて導入する場合はサブパスを使用し、dist/ 内部のファイルを参照しないでください:

ts
import { registerFileTools } from "@zqagent/core/tools/fs";
import { registerShellTool } from "@zqagent/core/tools/shell";
import { registerSearchTool } from "@zqagent/core/tools/search";
import { registerWebTools } from "@zqagent/core/tools/web";
import { registerGitTools } from "@zqagent/core/tools/git";
import { registerDiffTools } from "@zqagent/core/tools/diff";
import { registerTodoTool } from "@zqagent/core/tools/todo";
import { registerAskTool } from "@zqagent/core/tools/ask";
import { registerPlanTool } from "@zqagent/core/tools/plan";
import { registerDirectoryTool } from "@zqagent/core/tools/directories";
import { registerExploreTool } from "@zqagent/core/tools/explore";
import { registerMediaTool } from "@zqagent/core/tools/media";

internal/* サブパス(internal/engine/FileLogHook、internal/mcp/config など) は、同一リポジトリの server / CLI 用の非安定 APIであり、サードパーティは依存すべきではありません。本文では主入口 と上記の安定サブパスのみを使用します。FileLogHook、SavedState、MCP 設定関数などは実際には主入口 でもエクスポートされているため、主入口を優先してください。


4. 5分で動かす ​

以下は完全に実行可能な ESM スクリプトです(Node 18+):

ts
// agent-demo.mjs
import {
  ApprovalOutcome,
  EventType,
  Mode,
  createDefaultEngine,
  setQuestionHandler,
} from "@zqagent/core";

const engine = createDefaultEngine({
  workspace: process.cwd(),          // 权限边界:所有文件/命令都限制在这里
  model: process.env.OPENAI_MODEL!,  // 必填,不会兜底任何型号
  mode: Mode.INTERACTIVE,            // 有副作用的操作逐次审批
});

// 1) 审批回调:不注入时默认在第一次需要授权时抛错
engine.setApprover(async (request) => {
  console.log(`\n需要审批: ${request.toolName} — ${request.reason}`);
  console.log(request.arguments);
  return ApprovalOutcome.ONCE; // 生产环境这里接你的 UI / IM / 工单
});

// 2) ask_user 的回答回调(模块级全局,注入一次即可)
setQuestionHandler(async (question, options, allowText, multi) => {
  // 生产环境这里接你的 UI;示例直接返回第一个选项
  console.log(`模型提问: ${question}`, options);
  return options[0] ?? "";
});

// 3) 跑一轮,消费事件流
for await (const event of engine.run("读一下这个项目,告诉我它是做什么的")) {
  switch (event.type) {
    case EventType.ASSISTANT_DELTA:
      process.stdout.write(String(event.data.text ?? ""));
      break;
    case EventType.TOOL_STARTED:
      console.log(`\n[工具] ${event.data.name}`);
      break;
    case EventType.TOOL_FINISHED:
      console.log(`[工具结束] ${event.data.name}: ${event.data.status}`);
      break;
    case EventType.ERROR:
      console.error(`\n[错误] ${event.data.error}`);
      break;
    case EventType.TURN_END:
      console.log(`\n[结束] ${event.data.status}`);
      break;
  }
}

実行前に設定:

bash
export OPENAI_API_KEY=sk-...
export OPENAI_MODEL=gpt-4o-mini        # 或任意 OpenAI 兼容模型
node agent-demo.mjs

この1つのスクリプトで、組み込み時に最も重要な4つ——組み立て、承認、イベント消費、マルチターンコンテキスト (同じ engine でもう一度 run() を呼ぶと前ターンの履歴が引き継がれる)——をすでにカバーしています。


5. 中核概念 ​

以下の6つのオブジェクトを理解すれば、以降の API がすべて対応づけられます。

概念型責務
EngineTurnEngine1セッションのランタイム。メッセージ履歴、provider、ツール表、権限エンジンを保持
Turnengine.run() の1回の呼び出しユーザーの1入力 → 複数回の LLM/ツール反復 → 1つの turn_end
Iteration内部ループ1回の LLM 呼び出し + その応答が要求したすべてのツール実行
EventWsEventエンジンが外部に出力する唯一のリアルタイムチャネル(非同期ジェネレータ)
ToolToolSpecモデルが呼び出せる関数 + JSON Schema + リスクメタデータ
ApproverApprover権限エンジンが「人間への確認が必要」と判断した際に、呼び出し側が許可/拒否を決める

5.1 セッションと Engine は1対1 ​

セッションごとに1つの TurnEngine を作成し、複数セッションで同じ engine を共有しないでください:

  • engine.messages はセッション履歴であり、本質的に直列;
  • engine.permissions 内の「このセッションでは常に許可」はセッションレベルの状態;
  • engine.compactionState はセッションレベルの圧縮状態;
  • 同じ engine で同時に2つの run() を実行すると履歴が互相汚染する。

複数セッションの正しい方法は Map<sessionId, TurnEngine> で、セッションごとに1インスタンスを持たせます。詳細は 第18章を参照してください。

5.2 イベントストリームが主経路、Hook はバイパス ​

  • engine.run() の for await は主経路:順序性、バックプレッシャー、各イベントは1回だけ出現し、 SSE / WebSocket フレームに直接変換するのに適している。
  • EngineHook はバイパスコールバック:同期、戻り値なし、例外は飲み込まれ、同じデータの 第2消費側(保存、課金、ログ)に適している。hook だけに頼って UI を駆動しないでください。バックプレッシャーは保証されません。

6. エンジンの組み立て ​

2つの経路があります:

  • 経路 A:createDefaultEngine(opts) —— 1行で全組み込み機能を組み立て、90% のシーンに適する;
  • 経路 B:new TurnEngine(opts) —— provider、ツール表、権限エンジン、 システムプロンプトを自分で決め、深いカスタマイズや既存インフラの再利用に適する。

6.1 経路 A:createDefaultEngine ​

ts
import { createDefaultEngine, Mode } from "@zqagent/core";

const engine = createDefaultEngine({
  workspace: "/srv/workspaces/tenant-42",
  model: "deepseek-chat",
  mode: Mode.INTERACTIVE,
});

これは順に次の処理を行います(packages/core/src/index.ts:409 参照):

  1. provider が渡されていない場合 new OpenAIProvider({ logDir }) を作成;
  2. サンドボックス方案 resolveSandbox() を解決;
  3. ToolRegistry と ConflictGuard を作成し、21個の組み込みツールを登録(第7章参照);
  4. PermissionEngine を作成(ワークスペース、モード、コマンド許可リスト、shell ポリシー、サンドボックス状態);
  5. instructions が渡されていない場合 buildInstructions(agentType, workspace, today, lang) で システムプロンプトを生成;
  6. TurnEngine を組み立て;vcs が有効なら CompositeHook で VcsHook を並列接続;
  7. defaultApproval に従ってフォールバック approver を注入。

CreateEngineOpts 全パラメータ ​

パラメータ型 / デフォルト値説明
workspacestring(必須)ワークスペースルート = ファイルとコマンドの権限境界
modelstring、省略時は OPENAI_MODEL を読むモデル識別子。いずれも空の場合 run() が明確にエラーを出し、モデルをフォールバックしない
providerProviderClientカスタム provider。省略時は OpenAIProvider を作成
modeMode.INTERACTIVEDISCUSS / PLAN / INTERACTIVE / AUTO
maxIterationsDEFAULT_MAX_ITERATIONS = 800単一ターンの最大イテレーション回数
reasoningEffort省略時は OPENAI_REASONING_EFFORT を読むlow / medium / high。DeepSeek は自動スキップ
allowedCommands[]Auto モードで自動許可するコマンドプレフィックス
shellPolicy"approve"approve / sandbox / workspace / all
sandbox省略時 auto{ mode, extraWritable, ... }、8.4 参照
vcs{ mode: "off", sessionId: "default" }各ターンの Git スナップショット、第17章参照
instructionsbuildInstructions により生成システムプロンプトを上書き
agentType"code"code(プログラミング助手)/ cowork(汎用コラボレーション)、デフォルトプロンプトに影響
lang簡体字中国語zh / en / zh-TW / ja / ko / es、プロンプト、命名、要約の言語に影響
messages[]初期会話履歴(セッション復元時に渡す)
hookなしライフサイクルフック、第11章参照
subagentHookFactoryなしexplore 呼び出しごとにサブエージェント hook を生成
contextWindow自動検出、フォールバック 128Kモデルのコンテキストウィンドウ(token)
thresholdPct0.8ハード圧縮しきい値(ウィンドウに対する割合)
softThresholdPctハードしきい値の75%ソフトしきい値:古いツール結果のみを淘汰し、LLM は呼び出さない
capTokensnullハードしきい値の絶対上限(token)、null = 制限なし
toolResultDir状態ディレクトリ下の tool-results予算超過したツール結果の保存先。マルチテナントではテナントごとに分離必須
logDir記録しないLLM リクエスト監査ログのルートディレクトリ(日付別ディレクトリ)
defaultApproval"error"approver 未注入時:error / deny / allow

lang はデフォルトプロンプト、セッション命名、要約の言語にのみ影響します。明示的に instructions を渡した場合はそれが優先されます。

6.2 経路 B:手動で TurnEngine を組み立てる ​

ts
import {
  ApprovalOutcome,
  Mode,
  OpenAIProvider,
  PermissionEngine,
  ToolRegistry,
  TurnEngine,
  registerFileTools,
  registerShellTool,
  registerSearchTool,
} from "@zqagent/core";

const workspace = process.cwd();

// 1) 工具表:只注册你需要的
const registry = new ToolRegistry();
registerFileTools(registry, workspace);
registerShellTool(registry, workspace);
registerSearchTool(registry, workspace);

// 2) 权限:工作区 + 模式 + 白名单
const permissions = new PermissionEngine({
  workspaceRoot: workspace,
  mode: Mode.INTERACTIVE,
  allowedCommands: ["git status", "ls", "cat"],
});

// 3) Provider:任意 OpenAI 兼容端点
const provider = new OpenAIProvider({
  apiKey: process.env.OPENAI_API_KEY,
  baseURL: process.env.OPENAI_BASE_URL,
});

// 4) 引擎
const engine = new TurnEngine({
  provider,
  registry,
  permissions,
  model: process.env.OPENAI_MODEL!,
  instructions: "你是一个只处理数据分析任务的助手。",
  lang: "zh",
  maxIterations: 50,
});

engine.setApprover(async () => ApprovalOutcome.DENY); // 别忘了

TurnEngineOpts 全パラメータ ​

パラメータ必須説明
providerはいProviderClient(stream() / chat())
registryはいToolRegistry
permissionsはいPermissionEngine
modelはいモデル識別子。空文字列は run() でエラー
instructionsはいシステムプロンプト。先頭の system メッセージとして注入
genSessionNamePromptいいえセッション名生成プロンプト。省略時は lang に応じて取得
langいいえデフォルト "zh"。命名と圧縮要約に影響
maxIterationsいいえデフォルト 800
finalizeOnMaxIterationsいいえ上限到達後にツールなしの収尾ターンを1回実行。デフォルト false、サブエージェントは true
reasoningEffortいいえprovider に透過
messagesいいえ初期履歴。先頭が system でない場合、instructions を自動挿入
hookいいえEngineHook
contextWindowいいえ自動検出を上書き
thresholdPctいいえデフォルト 0.8
softThresholdPctいいえデフォルトはハードしきい値の75%
capTokensいいえデフォルト null
toolResultDirいいえツール結果の保存先ディレクトリ

6.3 実行時に変更できるもの ​

TurnEngine の公開属性とメソッド:

ts
engine.model = "another-model";          // 模型(下一轮生效)
engine.messages;                         // 完整历史(含 notice 消息)
engine.permissions.mode = Mode.AUTO;     // 运行模式
engine.registry;                         // 工具表(可动态 enable/disable)
engine.compactionState;                  // 压缩状态(只读观察)
engine.approver;                         // 当前审批回调

engine.setApprover(fn);                  // 设置审批
engine.setSystemPrompt(text);            // 运行时替换系统提示词(切 agent 用)
engine.requestInterrupt();               // 中断当前一轮
engine.queueSteering(text, attachments, refs); // 排队补充指令(后两个可选)
engine.removeSteering(text);             // 撤销尚未注入的补充指令

7. ツール:組み込みとカスタム ​

7.1 createDefaultEngine が登録する21個のツール ​

カテゴリツールリスク登録関数
ファイルread_file, list_files読み取り専用registerFileTools / registerReadOnlyFileTools
ファイルwrite_file, replace_in_fileローカル書き込みregisterFileTools
Shellrun_shell, shell_task_output, shell_task_kill実行registerShellTool
検索grep読み取り専用registerSearchTool
ネットワークweb_search, web_fetch外部registerWebTools
Gitgit_log, git_status, git_diff読み取り専用registerGitTools
パッチapply_patch, apply_unified_diffローカル書き込みregisterDiffTools
コラボレーションask_user, propose_plan, request_directoryインタラクションregisterAskTool / registerPlanTool / registerDirectoryTool
タスクtodo_write読み取り専用/状態registerTodoTool
サブエージェントexplore読み取り専用registerExploreTool
メディアmedia外部registerMediaTool

ターミナルツールは一括組み立てに含まれない:terminal_list / terminal_read はホストが提供する TerminalSource(コンソールの「ターミナル」パネルの常駐 PTY)に依存するため、自分で注入する必要があります:

ts
import { registerTerminalTools, type TerminalSource } from "@zqagent/core";

registerTerminalTools(registry, {
  terminals: myTerminalManager, // 实现 TerminalSource
  projectId: "tenant-42",
});

projectId は分離境界であり、必ずプロジェクトごとに渡してください。そうしないと A プロジェクトが B プロジェクトのターミナル内容を読めてしまいます。

各ツールの完全なパラメータと挙動は組み込みツールを参照してください。

7.2 カスタムツール ​

ToolRegistry.register のシグネチャ(packages/core/src/tools/ToolRegistry.ts:77):

ts
type Register = (
  func: (args: Record<string, unknown>, ctx?: { signal?: AbortSignal }) => unknown,
  metadata: {
    category: string;
    riskLevel: "low" | "medium" | "high";
    capabilities: string[];
    requiresApproval: boolean;
  },
  schema?: ToolSchema,   // 强烈建议显式给出
  source?: string,       // 展示用:"internal" / "mcp:xxx" / "myapp"
) => ToolSpec;

例:

ts
registry.register(
  async (args) => {
    const orderId = String(args.order_id);
    return await myOrderService.query(orderId);
  },
  {
    category: "business",
    riskLevel: "high",
    capabilities: ["query_order"],
    requiresApproval: true,
  },
  {
    type: "function",
    function: {
      name: "query_order",
      description: "按订单号查询内部订单系统,返回订单状态与金额",
      parameters: {
        type: "object",
        properties: {
          order_id: { type: "string", description: "订单号" },
        },
        required: ["order_id"],
      },
    },
  },
  "myapp",
);

注意事項:

  • ツール名は schema.function.name に由来します。schema を渡さない場合は関数名にフォールバックし、パラメータ構造が空になるため、モデルはほぼ正しく呼び出せません。そのため正式なツールでは必ず schema を明示してください。
  • func は Promise を返せます。エンジンが await します。
  • 第2引数 ctx.signal は中断シグナルです。長時間ツールはこれを基盤タスクに渡してください。そうしないとユーザー中断後もツールが最後まで実行されます。
  • 同名の register は古いツールを置換し、組み込み実装の上書きに使えます。remove() で削除、disable() / enable() で schemas() に表示されるか(モデルから見えるか)を制御します。
  • モデルに返す結果は結果予算を通ります。しきい値を超えると保存され、モデルには要約/パスが渡されます。環境変数 TOOL_RESULT_MAX_CHARS、TOOL_RESULT_SAVE_THRESHOLD、TOOL_RESULT_DIR で調整できます。
  • ツールが投げた例外はエンジンが捕捉し、{ error, error_type } をツール結果としてモデルに渡し、モデル自身に次のステップを判断させます。ターン全体がクラッシュすることはありません。

7.3 読み取り専用ツールセット ​

サブエージェントや読み取り専用シーン用:

ts
import { registerReadOnlyFileTools } from "@zqagent/core";
registerReadOnlyFileTools(registry, workspace); // read_file / list_files

8. 権限、承認、セキュリティ境界 ​

8.1 判断フロー ​

PermissionEngine.evaluate() は固定順序で判断します (packages/core/src/permissions/PermissionEngine.ts:173):

  1. 読み取り専用モード(DISCUSS / PLAN)→ 副作用のある操作は直接拒否;
  2. ファイル書き込みパスが workspaceRoot 配下にない → 直接拒否;
  3. 読み取り専用リスク(RiskClass.READ)→ 直接許可;
  4. AUTO モード → 副作用のある操作をすべて許可;
  5. shell 承認免除ポリシー(shellPolicy + サンドボックス)→ 直接許可される場合がある;
  6. コマンド許可リスト / セッションコマンド許可リスト → 直接許可;
  7. セッションツール許可リスト(ユーザーが「このツールを常に許可」を選択済み)→ 直接許可;
  8. その他 → needsUser: true、approver に委ねる。

戻り値の構造:

ts
interface Decision {
  allowed: boolean;
  reason: string;     // 展示给用户 / 审批卡片
  needsUser: boolean; // true = 需要审批
}

8.2 実行モード ​

ts
enum Mode {
  DISCUSS = "discuss",         // 只读,禁止写入与执行
  PLAN = "plan",               // 只读,禁止写入与执行
  INTERACTIVE = "interactive", // 有副作用操作逐次审批(推荐默认)
  AUTO = "auto",               // 有副作用操作全部放行
}

interactive はデフォルトであり、実際のユーザー向けに最も安全なモードです。auto は隔離環境、破棄可能なワークスペース、または完全に信頼できる入力でのみ使用してください。

8.3 承認コールバック ​

ts
type Approver = (request: PermissionRequest) => Promise<ApprovalOutcome>;

interface PermissionRequest {
  toolName: string;
  arguments: Record<string, unknown>;
  metadata: unknown;       // ToolMetadata | null
  reason: string;          // 权限引擎给出的原因
  toolCallId?: string;     // 与事件里的 reqId 对应
}

enum ApprovalOutcome {
  ONCE = "once",                   // 仅本次
  ALWAYS_TOOL = "always_tool",     // 本会话总是允许该工具
  ALWAYS_COMMAND = "always_command", // 本会话总是允许该命令(仅 run_shell)
  DENY = "deny",                   // 拒绝
}

実装方式:

  • CLI / デスクトップ:ユーザーが UI でボタンを押すまでブロックして待機;
  • Web サービス:リクエストを Promise に入れ、クライアントに承認カードをプッシュし、コールバック受信後に resolve ——これが @zqagent/server の「一時停止型承認」です;
  • 無人運用 Worker:許可リストに従って自動的に ONCE を返し、それ以外は DENY(または defaultApproval: "allow"。ただしワークスペースが隔離されていることを先に確認)。

approver の実装は必須です。 デフォルトの defaultApproval: "error" は最初に承認が必要になった時点でエラーを投げます。 これは「承認接続を忘れる → ツールが静かに実行されない → ユーザーはモデルが壊れたと誤解する」ことを避けるための意図的な設計です。

8.4 shell の承認免除と OS サンドボックス ​

shellPolicy:

値挙動
approve(デフォルト)許可リスト以外は一律承認
sandboxOS レベルのサンドボックスが実際に有効な場合のみ承認免除、それ以外は承認
workspaceサンドボックス有効 → 承認免除;無効 → ワークスペースパス判定でフォールバック
allshell をすべて承認免除(高リスク、shell にのみ効く auto と同等)

サンドボックス設定:

ts
createDefaultEngine({
  workspace,
  sandbox: {
    mode: "auto",                  // auto / off / required
    extraWritable: ["/data/cache"], // 额外可写路径
  },
});
  • macOS:sandbox-exec で file-write* をワークスペース + 一時ディレクトリに限定;
  • Linux:bwrap でシステム全体を読み取り専用マウント + ワークスペース書き込み可能;
  • Windows:同等のプロセスレベルファイルシステムサンドボックスはなく、enforced: false となりパスヒューリスティックに退化します(これは煩わしさを減らす戦略であり、セキュリティ境界ではありません)。

required モードではサンドボックスを検出できない場合に明確にエラーを出します。「必ず隔離する」ことをコンプライアンス要件とするデプロイに適しています。

8.5 コマンド許可リスト ​

ts
createDefaultEngine({
  workspace,
  mode: Mode.AUTO,
  allowedCommands: ["git status", "git diff", "ls", "cat", "pnpm test"],
});

ルール:

  • プレフィックス一致ですが、単語境界で終わる必要があります。ls は lsof を許可しません;
  • shell 演算子(; & | > < \ $( ( \n)を含むコマンドは**一律に自動許可しません**。ls; rm -rf /` のような回避を防ぎます;
  • セッション内でユーザーが「このコマンドを常に許可」を選んだ後、実行可能プログラム名で記憶し、単純なコマンドにのみ有効です。

8.6 セキュリティ境界チェックリスト ​

  • workspace を /、ユーザーホームディレクトリ、または鍵を含むディレクトリに設定しない;
  • デフォルトは Mode.INTERACTIVE を使い、「自動化体験」のためにデフォルトを AUTO にしない;
  • マルチテナントではテナントごとに独立した workspace、toolResultDir、FileLogHook ディレクトリ、MCP 設定;
  • MCP ツールはデフォルトで requiresApproval: true。承認を全体として無効にしない;
  • logs / logDir は LLM リクエスト内容を記録するため、データ分類に応じて有効化の可否と保持期間を決定;
  • ツールが返す Web ページ/ファイル内容は信頼できないデータであり、命令として実行しない(エンジンのプロンプトに関連制約は含まれていますが、業務側でも出力検証を行うべきです)。

9. イベントストリーム完全リファレンス ​

9.1 構造 ​

ts
interface WsEvent {
  type: EventType;
  data: Record<string, unknown>; // 字段随类型变化
}

run() は AsyncGenerator<WsEvent> を返します。最後まで消費する必要があります(または break 後にジェネレータを回収させる)。そうしないとエンジンは先へ進みません。典型的な順序:

turn_start
  ├─ (compacting → compacted)?          # 上下文接近阈值时
  ├─ llm_request
  ├─ assistant_delta / reasoning_delta* # 流式增量
  ├─ assistant_message                  # 本轮完整回复
  ├─ tool_proposed
  ├─ (permission_required → approver)?  # 需要审批时
  ├─ tool_started
  ├─ tool_finished
  ├─ iteration_end
  └─ 回到 llm_request(还有工具结果要喂回模型时)
turn_end

異常経路:error の後にも turn_end が補われます。中断経路:interrupted の後にも turn_end が補われます。消費側はストリーム終了ではなく turn_end を1ターンの状態機械の終態とすべきです。

9.2 イベント種別とフィールド ​

イベントdata の主要フィールド発生タイミング
turn_starttaskId, input, attachmentCount?, attachments?各ターン開始
assistant_deltatext, reqId, taskIdモデルのストリーミングテキスト増分
reasoning_deltatext, reqId, taskId推論過程の増分(一部モデル)
assistant_messagetext, reasoning, tool_calls(ツール名配列), usage?, reqId, taskId, logTime1回の LLM 応答終了
tool_proposedreqId(=ツール呼び出し ID), name, arguments, taskIdモデルがツール呼び出しを提案
permission_requiredtaskId, reqId, name, arguments, reason, category承認が必要(その後 approver を呼び出す)
tool_startedtaskId, reqId, name, startTime?ツール実行開始
tool_finishedtaskId, reqId, name, status, reason?, duration?, result_preview?ツール終了
iteration_enditeration, taskId1イテレーション(LLM + ツール)終了
turn_endstatus, iterations, produced_files, taskId1ターン終了(必ず出力)
errorerror, error_type, retriable, iteration, reqId, logTime, taskId, stack?エンジンエラー
interruptediterations, reqId, taskIdユーザー中断
compactingtaskId, trigger, tokens, estimated, contextWindow, thresholdPct, thresholdTokens, outboundMessages圧縮開始
compactedreqId, taskId, method, messagesBefore, messagesAfter, boundaryIndex?, model?, summaryPreview?, keptUserMessages?, droppedUserMessages?圧縮完了
queue_steeringtext, taskId, attachments?キューされた補充指令が注入された
llm_requestreqId, taskId, logTime, model, msgCount, toolCount, compact?各 LLM リクエスト前(監査用)
ask_user_questiontaskId, question, options, allowText, multiモデルが ask_user を呼び出した
plan_proposedplanモデルが propose_plan を呼び出した
directory_requestedreason, path, writableモデルが request_directory を呼び出した
llm_retryattempt, max, reason, waitMs, iteration, reqId一時的なネットワークエラーで自動リトライ
session_namedname, source列挙は予約済み。現在は CLI/server が命名後に自らブロードキャストし、TurnEngine は出力しない

tool_finished.status の値:

status意味
ok実行成功
errorツールが例外を投げた、またはエラーを返した
denied権限拒否またはユーザー拒否
interrupted実行中に中断

turn_end.status の値:

status意味
completedモデルがツールを要求しなくなり、正常終了
max_iterations_exceededmaxIterations に到達
interruptedユーザー中断
errorLLM/エンジンエラーによる終了

9.3 消費例:SSE に変換 ​

ts
import type { WsEvent } from "@zqagent/core";

function toSse(event: WsEvent): string {
  return `event: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`;
}

app.post("/sessions/:id/messages", async (req, res) => {
  const engine = sessions.get(req.params.id)!.engine;
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });
  for await (const event of engine.run(String(req.body.text))) {
    res.write(toSse(event));
  }
  res.end();
});

注意:SSE の切断はエンジンを自動停止しません。クライアント切断時には engine.requestInterrupt() を呼び出してください。そうしないとエンジンは最後まで実行を続け、token を消費します。

9.4 エラーとリトライ ​

  • error.retriable === true:engine.retry() を試せます(前ターンがエラーで終了し、末尾がリトライ可能なエラーである場合のみ有効)。
  • 429/5xx は OpenAI SDK 自身がリトライします。一時的なネットワークエラーはエンジンが自動的に NETWORK_RETRY_MAX(デフォルト 2)回リトライし、バックオフ基数は NETWORK_RETRY_BASE_MS(デフォルト 800ms)で、その間 llm_retry を発行します。
  • コンテキストのオーバーフローは自動圧縮後にリトライし、外部にはエラーを出しません。圧縮も失敗した場合にのみエラーを出し、リトライ可能とマークします。

10. モデル Provider ​

10.1 OpenAIProvider ​

ts
import { OpenAIProvider } from "@zqagent/core";

const provider = new OpenAIProvider({
  apiKey: process.env.OPENAI_API_KEY,   // 缺省读同名环境变量
  baseURL: process.env.OPENAI_BASE_URL, // 任意 OpenAI 兼容端点
  logDir: "/var/log/zqagent/llm",       // 可选:请求审计日志
});

挙動の要点:

  • Responses API 優先。失敗時はエラー種別に応じて Chat Completions に降格:
    • エンドポイント非対応(404/405 など)→ 降格し、モデルごとにキャッシュ;
    • パラメータ非互換(一部の互換ゲートウェイ)→ 今回のみ降格;
    • 401/429/5xx/ネットワークエラー → 降格せず、そのまま送出。
  • 環境変数 OPENAI_FORCE_API_MODE=responses|chat でモードを強制できます。
  • モデル名または baseURL に deepseek が含まれる場合、Responses 経路はサーバー側 web_search 組み込みツールを自動注入し、ローカルの同名ツールをフィルタします。
  • logDir は日付別ディレクトリに分け、reqId ごとにリクエスト/レスポンス監査ファイルを書き出します。メッセージ内容を含むため、データコンプライアンス要件に応じて有効化を判断してください。

関連環境変数:

変数作用
OPENAI_API_KEYAPI Key
OPENAI_BASE_URL互換エンドポイント
OPENAI_MODELデフォルトモデル
OPENAI_REASONING_EFFORTlow / medium / high
OPENAI_FORCE_API_MODEresponses / chat
OPENAI_MAX_RETRIESSDK リトライ回数
OPENAI_TIMEOUT_MSリクエストタイムアウト
OPENAI_STREAM_IDLE_MSストリームアイドルウォッチドッグ
SOCKS5_PROXYLLM とネットワークツールが SOCKS5 プロキシを使用

10.2 カスタム Provider ​

エンジンは2つのメソッドにのみ依存します(packages/core/src/providers/OpenAIProvider.ts):

ts
// 注意:StreamOpts 目前未从包根导出;实现自定义 provider 时,
// 按下面用到的字段自行声明即可(reqId / logTime 必填,其余可选)。
interface ProviderClient {
  stream(
    model: string,
    messages: Message[],
    tools: ToolSchema[],
    opts: StreamOpts,
  ): Promise<AsyncIterable<StreamChunk>>;

  chat(
    model: string,
    messages: any[],
    options: RequestOptions,
    opts: StreamOpts,
    params?: { maxTokens?: number },
  ): Promise<string | null>;
}

StreamChunk:

ts
interface StreamChunk {
  content: string | null;
  reasoning: string | null;
  toolCalls: ToolCall[] | null; // 仅流结束 chunk 给完整列表
  usage: TokenUsage | null;
  finishReason: string | null;
}

カスタム provider 実装時の最重要ポイント2つ:

  1. 最後の chunk には toolCalls または finishReason が必須。そうでないとエンジンはストリームが正常終了していないと判断します;
  2. opts.signal の abort を受けたら直ちに読み取りを停止し、例外を投げる/ストリームを終了する。そうでないと「停止」ボタンが無効になります。

chat() は主にセッション命名などの軽量リクエストに使われます。null を返しても主フローには影響しません。

10.3 推論の強度 ​

ts
createDefaultEngine({
  workspace,
  reasoningEffort: "high", // low / medium / high
});

provider に透過されます。OpenAI 系モデルでは有効で、DeepSeek は自動スキップされます(thinking スイッチを使い、レベル制御には対応していません)。


11. ライフサイクルフックと永続化 ​

11.1 EngineHook の全コールバック ​

コールバック引数用途
onTurnStartinput1ターン開始
onTurnEndstatus, iterations, producedFiles1ターン終了
onAssistantDeltatextストリーミングテキスト
onReasoningDeltatextストリーミング推論
onAssistantMessagetext, toolCalls, usage?完全な応答(課金でよく使用)
onToolProposedname, argsツール提案
onToolStartednameツール開始
onToolFinishedname, status, resultPreviewツール終了
onPermissionRequiredname, reason承認が必要
onIterationEnditerationイテレーション終了(チェックポイントでよく使用)
onCompacting / onCompactedinfo / text, info圧縮
onErrorerror, errorType, info?エラー(retriable を含む)
onRetryattempt, max, reasonネットワーク自動リトライ
onInterruptediterations中断
onEventtype, data未マッチイベントのフォールバック

フックは同期コールバックであり、エンジンは await しません。したがって:

  • hook 内で長時間ブロックする操作をしない(エンジンが詰まります);
  • hook が投げた例外はエンジンに飲み込まれ(_emitHook 内部の try/catch)、主フローに影響しません;
  • バックプレッシャー/順序保証が必要なデータは、hook ではなく run() のイベントストリームを消費してください。

11.2 複数 Hook の組み合わせ ​

ts
import { CompositeHook, ConsoleHook, FileLogHook } from "@zqagent/core";

const hook = new CompositeHook([
  new ConsoleHook(),
  new FileLogHook(sessionId, "/var/lib/myapp/sessions"),
]);

createDefaultEngine({ vcs }) の内部でも CompositeHook を使い、VcsHook を呼び出し側自身の hook に並列接続し、上書きしません。

11.3 FileLogHook:ファイルレベルの永続化と復元 ​

ts
import { FileLogHook } from "@zqagent/core";

const hook = new FileLogHook(sessionId, stateDir);
// 写入:
//   <stateDir>/<sessionId>.jsonl      事件日志(JSONL)
//   <stateDir>/<sessionId>_state.json 检查点

const engine = createDefaultEngine({
  workspace,
  messages: FileLogHook.loadState(sessionId, stateDir)?.messages,
  hook,
});

// 关键节点保存状态(通常在 onIterationEnd / onTurnEnd 里调)
hook.saveState(engine, { name, nameSource: "auto", taskCount });

SavedState のフィールド:

ts
interface SavedState {
  sessionId: string;
  model: string;
  messages: Message[];
  compaction: Record<string, unknown> | null;
  savedAt: string;
  name?: string;
  nameSource?: "auto" | "user";
  taskCount?: number;
  permissionMode?: string;
  allowTools?: string[];
  allowCommands?: string[];
}

復元時の注意:

  • messages を new TurnEngine({ messages }) または createDefaultEngine({ messages }) に渡す;
  • permissionMode と許可リスト(engine.permissions.mode = ...、 allowToolForSession / allowCommandForSession)を復元する。そうしないと再起動後に以前のユーザー承認が失われます;
  • FileLogHook.loadState() はファイルを読めない場合 null を返し、エラーを投げません;
  • マルチテナントではセッションごとに1つの stateDir とし、ディレクトリを混在させない。

FileLogHook は onPaused / onResumed メソッドも提供し、一時停止/再開の記録に使えますが、これらは EngineHook インターフェースには含まれないため、必要に応じてインスタンスメソッドを直接呼び出してください。

11.4 イベントログの用途 ​

JSONL イベントログは監査(誰がいつ Agent に何をさせたか)とリプレイの基礎です。セッション状態の唯一の情報源ではありません。ファイル内容の変更には第17章の VCS スナップショットを使用してください。


12. 1ターン Turn の制御 ​

12.1 マルチターン会話 ​

ts
import type { WsEvent } from "@zqagent/core";

async function drain(stream: AsyncIterable<WsEvent>): Promise<void> {
  for await (const event of stream) {
    // 渲染或忽略
  }
}

await drain(engine.run("先看一下项目结构"));
await drain(engine.run("把 README 里的示例改成 TypeScript")); // 自动带上一轮上下文

同じ engine を連続して呼び出すと履歴が保持されます。新しいセッションを開始するには新しい engine を作成します。

12.2 中断 ​

ts
// 在另一个地方(例如 HTTP DELETE / 用户点「停止」)
engine.requestInterrupt();

これは次を行います:

  1. キャンセルフラグを立てる;
  2. 実行中の LLM ストリームを abort;
  3. 実行中のツールを abort(run_shell は子プロセスツリーを kill);

その後、イベントストリームは interrupted + turn_end(status: "interrupted") を出力します。 「イベントストリームの消費停止」を中断の代わりにしないでください。その場合エンジンは進行せず、状態もクリーンアップされません。

12.3 リトライ ​

ts
for await (const e of engine.retry()) {
  // 仅在上一轮以可重试错误结束时才会产出事件
}

12.4 一時停止した承認からの復元 ​

engine.resume() は「履歴末尾に未回答のツール呼び出しがある」シーンに使います(典型的にはサービス再起動後に一時停止した承認を復元する場合):

ts
for await (const e of engine.resume()) {
  // 重新处理挂起的 tool_calls;approver 会按已 resolve 的结果立即返回
}

一時停止中のツール呼び出しがない場合、resume() は直接戻り、イベントを出力しません。

12.5 Steering:実行中の追加入力 ​

ts
engine.queueSteering("顺便把测试也跑一下");
engine.queueSteering("忽略 dist 目录", attachments, refs);

エンジンはイテレーションの合間にキューされた指令を user メッセージとして注入し、queue_steering を出力します。 「Agent が長いタスクを実行中に、ユーザーが一言補足したい」というインタラクションに適しています。まだ注入されていない指令は取り消せます:

ts
engine.removeSteering("顺便把测试也跑一下"); // 已注入返回 false

12.6 並行性の制約 ​

  • 同じ engine で同時に実行できるターンは1つだけ;
  • ツールレベル:低リスクツールは並行実行され、高リスク/承認要/インタラクション系ツールは直列実行される;
  • サービス層では同一セッションの run() をキュー化し、2つのリクエストが同時に messages を書き込むのを避けるべきです。

13. 添付ファイル ​

13.1 データ構造 ​

ts
interface MessageAttachment {
  name: string;
  mimeType: string; // image/* audio/* video/*
  dataUrl: string;  // data:<mime>;base64,<payload>
}

interface StoredAttachment {
  name: string;
  mimeType: string;
  size: number;
  path: string; // 相对会话目录,如 "attachments/xxx-0-shot.png"
}

制限(packages/core/src/engine/attachments.ts):

定数値意味
MAX_ATTACHMENTS41回あたり最大添付数
MAX_ATTACHMENT_BYTES10 MB単一ファイルかつ合計上限
ATTACHMENT_DIR"attachments"セッションディレクトリ下のサブディレクトリ

13.2 推奨フロー:先に保存し、参照を渡す ​

ts
import {
  normalizeAttachments,
  saveAttachments,
  type MessageAttachment,
  type StoredAttachment,
} from "@zqagent/core";

// 1) 校验并归一化(非法会抛 AttachmentError)
const attachments: MessageAttachment[] = normalizeAttachments(rawInput) ?? [];

// 2) 存到会话目录
const refs: StoredAttachment[] = saveAttachments(sessionDir, attachments);

// 3) 跑一轮:base64 只在本轮发给模型,历史轮次会自动剥离
for await (const e of engine.run(text, taskId, attachments, refs)) {
  // turn_start.data.attachmentCount / attachments(refs)
}

StoredAttachment はメッセージとイベントに伴って流れ、UI でサムネイルを描画するのに使えます。バイト本体は <sessionDir>/attachments/ にのみ存在します。読み取り時は次を使います:

ts
const { name, mimeType, size, base64 } = readStoredAttachment(sessionDir, ref.path);

セキュリティ上の注意:

  • readStoredAttachment は attachments/<file> 形式のみ受け付け、.. とサブディレクトリを拒否します;
  • 添付ファイルは LLM リクエストに入り、logDir 監査ログはメッセージを記録します(base64 を記録するかは provider のログ実装次第であり、本番環境では評価すべきです);
  • ユーザーがアップロードした任意の HTML/スクリプトをテキスト添付として直接レンダリングしないでください。

14. コンテキスト圧縮 ​

長いセッションをモデルウィンドウに無限に詰め込むことはできないため、エンジンは自動的に圧縮します。

14.1 トリガー機構 ​

概念デフォルト説明
コンテキストウィンドウ自動検出、フォールバック 128KcontextWindow で上書き可能
ハードしきい値ウィンドウの80%thresholdPct
ソフトしきい値ハードしきい値の75%(≈60% ウィンドウ)softThresholdPct、古いツール結果のみ淘汰
絶対上限なしcapTokens
最近保持約25%圧縮後に保持する直近会話の割合
要約出力上限3000 token内部固定

フロー:

  1. ソフトしきい値到達:古いツール結果を淘汰し、重複する read_file を去重し、LLM を呼び出さない;
  2. ハードしきい値到達:LLM を呼び出して要約を生成し、compacting / compacted を出力;
  3. API がコンテキストオーバーフローを報告:強制圧縮後にリトライ。

14.2 イベントとフック ​

ts
// engine.run(...) 的事件流中会出现:
// { type: "compacting", data: { trigger: "threshold", tokens, contextWindow, ... } }
// { type: "compacted",  data: { method: "summarized" | "trimmed", messagesBefore, messagesAfter, ... } }

onCompacting / onCompacted フックは同じ情報を受け取ります。UI はこれに基づいて「コンテキストを圧縮中」と表示できます。

14.3 調整可能パラメータと環境変数 ​

ts
createDefaultEngine({
  workspace,
  contextWindow: 200_000,
  thresholdPct: 0.85,
  softThresholdPct: 0.7,
  capTokens: 120_000,
});
環境変数作用
COMPACTION_MODEL要約に使用するモデル(省略時は現在のモデルを継続使用)
COMPACTION_REASONING_EFFORT要約の推論強度(デフォルト low)
COMPACTION_ELIDE0 で古いツール結果の淘汰を無効化
COMPACTION_DEDUP0 で重複読み取りの去重を無効化
CONTEXT_WINDOWサーバー側組み立て時のウィンドウ上書き(@zqagent/server が読む)
COMPACTION_THRESHOLD_PCT / COMPACTION_SOFT_THRESHOLD_PCT / COMPACTION_CAP_TOKENSサーバー側組み立て時のしきい値上書き

CompactionState と圧縮ツール関数は現在パッケージルートからエクスポートされていません。サードパーティは engine.compactionState で状態を観察するか、イベント/hook に依存するだけで十分です。深いパスで内部実装をインポートしないでください。


15. MCP 統合 ​

MCP(Model Context Protocol)により、Agent は外部ツールサーバーを利用できます。@zqagent/core にはクライアントとツール登録ロジックが含まれています。

15.1 最小構成 ​

ts
import {
  McpManager,
  ToolRegistry,
  defaultMcpConfigPath,
  loadMcpConfigFile,
  withBuiltinMcpConfigs,
} from "@zqagent/core";

const registry = new ToolRegistry();
const { configs, warnings } = loadMcpConfigFile(
  defaultMcpConfigPath(workspace), // <workspace>/.agent/mcp.json
);
const manager = new McpManager(registry, withBuiltinMcpConfigs(configs), {
  builtin: {
    workspace,
    env: process.env,
    llm: { apiKey, baseURL, model }, // 内置 OCR 的多模态兜底
  },
});

const { connected, failed } = await manager.connectAll();

接続完了後、registry.schemas() には MCP ツールが自動的に含まれ、モデルは次の LLM 呼び出しでそれらを認識できます(engine を再構築する必要はありません)。

15.2 設定形式 ​

<workspace>/.agent/mcp.json は複数の記法をサポートします:

jsonc
// 裸数组
[
  { "name": "filesystem", "type": "stdio", "command": ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/data"] },
  { "name": "remote", "type": "http", "url": "https://example.com/mcp", "headers": { "Authorization": "Bearer ${TOKEN}" } }
]

// 或 { "servers": [...] } / { "servers": { "name": {...} } }
// 或 Claude/Cursor 风格 { "mcpServers": { "name": { "command": "npx -y pkg", "args": ["..."] } } }
  • type 省略時は builtin → url → command の順で推論;
  • command は文字列または配列にできる;
  • ${VAR} / ${env:VAR} は環境変数で展開される;
  • enabled: false または disabled: true で無効化;
  • 組み込みプリセット(OCR)はユーザーに同名設定がない場合に自動追加され、{ "name": "ocr", "enabled": false } で無効化できます。

15.3 ツール命名と承認 ​

  • 登録名形式:mcp__<server>__<tool>(両セグメントとも文字サニタイズと切り詰めが行われる);
  • メタデータ:category: "mcp"、riskLevel: "high"、requiresApproval: true;
  • したがってapprover の接続が必須です。そうでないとすべての MCP ツールがデフォルトのフォールバック戦略になります。

15.4 遅延読み込み(ツールが多い場合) ​

ts
new McpManager(registry, configs, {
  lazy: {
    enabled: true,       // 或交给阈值自动判断
    thresholdTools: 16,
    thresholdChars: 24_000,
    maxLoaded: 40,
  },
});

有効化すると2つのメタツールのみ登録されます:

  • mcp_search_tools(読み取り専用、承認免除):キーワードでツールを検索し、ヒットした先頭8件を自動ロード;
  • mcp_call_tool(承認要):名前でロード済み/解決可能なツールを呼び出す。

環境変数 ZQAGENT_MCP_LAZY=1/0 で強制的にオン/オフできます。

15.5 ライフサイクル ​

ts
await manager.connectServer("github");
await manager.refresh("github");   // 断开 → 重连 → 重新注册
manager.attach(newRegistry);       // 引擎重建时迁移已加载工具
await manager.closeAll();          // 进程退出时

McpManager はメモリ管理のみを行い、設定を永続化しません。addConfig / updateConfig / removeConfig の後は自分で saveMcpConfigFile() を呼び出す必要があります。

15.6 組み込み OCR ​

ocr プリセットは mcp__ocr__recognize を公開し、画像/PDF の文字抽出をサポートします。macOS Vision → tesseract → マルチモーダル LLM の順で降格します。builtin.llm には現在のセッション の provider 設定を渡すべきです。そうでないと第3段階の降格が利用できません。


16. サブエージェントと Agent レジストリ ​

16.1 explore サブエージェント ​

createDefaultEngine は explore ツールを登録済みです(packages/core/src/tools/explore.ts):

  • メインエージェントが「調査タスク」をサブエージェントに渡す;
  • サブエージェントは独立したコンテキスト、独立したツール表を使用し、Mode.PLAN(書き込みをハードブロック)で実行される;
  • デフォルトでは読み取り専用ファイルツール、検索、Git 読み取り専用ツールのみを持つ;
  • デフォルト最大30ターン(EXPLORE_MAX_ITERATIONS)。上限に達すると収尾のためもう1ターン実行し、レポートを出力する;
  • メインエージェントは最終レポートのみを受け取り、サブエージェントの中間過程はメインコンテキストに入らない。

サブエージェントの挙動をカスタマイズ:

ts
createDefaultEngine({
  workspace,
  subagentHookFactory: () => new SubagentHook("explore"), // 每个子代理一个实例
});

subagentHookFactory を渡さない場合、サブエージェントは完全にサイレントになります。SubagentHook はサブエージェントのツール呼び出し をインデントしてターミナルに出力する CLI の参考実装です。Web シーンでは独自の hook を実装し、サブエージェントイベント をフロントエンドに転送できます(注意:サブエージェントイベントはメインの run() のイベントストリームを通りません)。

手動登録:

ts
registerExploreTool(registry, {
  workspace,
  provider,
  model,
  lang: "zh",
  hookFactory: () => mySubagentHook,
  toolResultDir,
});

16.2 AgentRegistry ​

AgentRegistry はメモリ内のロール表であり、「名前 + 説明 + システムプロンプト + 任意のモデル」を管理します:

ts
import { AgentRegistry } from "@zqagent/core";

const agents = new AgentRegistry(); // 默认内置 code / cowork
agents.add({ name: "analyst", description: "数据分析", prompt: "你是数据分析师…" });
agents.update("analyst", { model: "deepseek-chat" });
agents.switch("analyst");

const prompt = agents.buildSystemPrompt("analyst", workspace, today, "zh");
engine.setSystemPrompt(prompt); // 切换角色
engine.model = agents.currentDef.model ?? engine.model;

AgentRegistry はメモリ管理のみを行い、永続化(例:agents.json)は呼び出し側が実装します。


17. 変更追跡とロールバック ​

Agent はファイルを変更します。イベントログは「ツールの意図」のみを記録し、ファイル内容は記録しません。「このターンで結局何が変わったのか、どうロールバックするのか」に答えるには、VCS スナップショットを使います。

17.1 有効化 ​

ts
const engine = createDefaultEngine({
  workspace,
  vcs: { mode: "auto", sessionId: "tenant-42-session-7" },
});

mode:

値挙動
off(デフォルト)無効
autoGit リポジトリならネイティブモード、そうでなければ shadow リポジトリ
nativeワークスペースの .git を使うが、独立した index と refs/agent/* を使用し、ユーザーの HEAD/staging には触れない
shadow<workspace>/.agent/vcs にシャドウリポジトリを作成

native / shadow はどちらもローカルに git が必要です。見つからない場合は off に退化します(書き込み競合検出は引き続き利用可能)。

17.2 照会とロールバック ​

ts
import { SessionVcs, resolveVcsPlan } from "@zqagent/core";

const vcs = new SessionVcs(resolveVcsPlan(workspace, { mode: "auto" }), sessionId);

const turns = await vcs.listTurns();       // 只列「有改动的轮次」,序号从 1 开始
const patch = await vcs.diffTurn(2);       // git show --patch
const result = await vcs.restoreTurn(2, {   // 回滚到第 2 轮之后的状态
  paths: ["src/app.ts"],                    // 可选:只回滚部分文件
  dryRun: false,
});
// result: { restored, overwritten, missing }

await vcs.forget();  // 删除该会话的 refs/agent/<id>,不还原文件

VcsHook は onTurnStart → beginTurn()、onTurnEnd → commitTurn(status) を接続済みです。createDefaultEngine({ vcs }) は自動的にこれを組み合わせます。ライフサイクルを自分で管理する必要がある場合は、 直接 new VcsHook(vcs) を作成し、hook の組み合わせに含めてください。

注意:

  • commitTurn() が返す turn は常に 0 です。実際のターン番号は listTurns() から得られます;
  • diffTurn / restoreTurn の turn は「変更のあるターン」の番号であり、空ターンは番号を消費しません;
  • スナップショット内容は git add -A のワークスペース状態で、未追跡ファイルを含みます(.agent/、.git/、node_modules/ などの除外ルールに従います)。

18. 4つの統合モードとコード骨子 ​

18.1 モード1:HTTP サービス + SSE(@zqagent/server に最も近い) ​

重要な設計:

  • Map<sessionId, SessionRuntime>。各 runtime は engine、イベントバッファ、承認待ち Promise を保持;
  • 同一セッションの run() は直列にキューイング;
  • 承認は permission_required イベント + resolve 待ち Promise で「一時停止」を実現;
  • クライアント切断時に requestInterrupt()。
ts
import {
  ApprovalOutcome,
  Mode,
  createDefaultEngine,
  type PermissionRequest,
} from "@zqagent/core";

interface Runtime {
  engine: ReturnType<typeof createDefaultEngine>;
  queue: Promise<void>;
  pendingApproval?: {
    request: PermissionRequest;
    resolve: (o: ApprovalOutcome) => void;
    reject: (e: Error) => void;
  };
}

const sessions = new Map<string, Runtime>();

function getRuntime(sessionId: string, workspace: string): Runtime {
  let rt = sessions.get(sessionId);
  if (rt) return rt;

  const engine = createDefaultEngine({
    workspace,
    mode: Mode.INTERACTIVE,
    toolResultDir: `${workspace}/.agent/tool-results`,
  });

  const runtime: Runtime = { engine, queue: Promise.resolve() };

  engine.setApprover((request) => {
    return new Promise<ApprovalOutcome>((resolve, reject) => {
      runtime.pendingApproval = { request, resolve, reject };
      // 通过 SSE/WebSocket 把 request 推给前端;前端 POST /approve 时:
      //   runtime.pendingApproval?.resolve(ApprovalOutcome.ONCE)
      // 超时或连接断开时:
      //   runtime.pendingApproval?.resolve(ApprovalOutcome.DENY)
    });
  });

  sessions.set(sessionId, runtime);
  return runtime;
}

// POST /sessions/:id/messages
// res 为 Express/Fastify 等框架的响应对象;这里只要求 write/end 两个方法
async function send(
  sessionId: string,
  workspace: string,
  text: string,
  res: { write(chunk: string): void; end(): void },
) {
  const rt = getRuntime(sessionId, workspace);
  const task = rt.queue.then(async () => {
    for await (const event of rt.engine.run(text)) {
      res.write(`event: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`);
    }
  });
  rt.queue = task.catch(() => {}); // 保证下一条消息不被前一条的异常卡住
  await task;
}

18.2 モード2:デスクトップアプリ(Electron / Tauri sidecar) ​

  • エンジンはメインプロセスまたは sidecar プロセス内で動作し、UI は IPC 経由でイベントを受信;
  • workspace はユーザーが選択した作業ディレクトリを指す;
  • 承認にはネイティブダイアログを使用;
  • ファイルツールと shell はローカルマシンで直接実行されるため、必ず INTERACTIVE とサンドボックスを維持;
  • 終了前に await mcpManager.closeAll() を実行し、hook.saveState() を呼び出す。

18.3 モード3:無人運用 Worker ​

ts
const engine = createDefaultEngine({
  workspace: jobWorkspace,
  mode: Mode.INTERACTIVE,
  defaultApproval: "deny", // 没人可问:默认拒绝
  allowedCommands: ["npm test", "npm run build"],
});

engine.setApprover(async (req) => {
  // 用业务规则做静态审批,而不是问人
  if (req.toolName === "run_shell" && String(req.arguments.command).startsWith("npm test")) {
    return ApprovalOutcome.ONCE;
  }
  return ApprovalOutcome.DENY;
});

Worker シーンでは追加で注意:

  • 各 job に独立した workspace を用意し、終了後にクリーンアップまたはアーカイブ;
  • maxIterations、contextWindow、capTokens の上限を設定し、token の暴走を防ぐ;
  • onAssistantMessage で使用量を集計;
  • onTurnEnd で status を判定し、completed 以外は失敗として記録。

18.4 モード4:マルチテナント SaaS ​

必ず分離すべき次元:

リソース分離方式
ファイルシステムテナントごとに独立した workspace
ツール結果の保存テナント/セッションごとに独立した toolResultDir
イベントとチェックポイントセッションごとに独立した stateDir(FileLogHook)
Git スナップショットセッションごとに sessionId → refs/agent/<id>
MCPテナントごとの設定ソースと McpBuiltinContext.workspace
承認承認リクエストにはテナント/ユーザーコンテキストが必須で、キューをグローバル共有しない
モデル資格情報テナントごとの provider インスタンス。key を prompt/ツール引数に入れない

テナントを跨いで engine、registry(MCP ツールが相互汚染する)、permissions(許可リストが混線する)を再利用しないでください。


19. 本番チェックリスト ​

リリース前に項目ごとに確認:

組み立て

  • [ ] model を明示的に設定し、未設定時は起動時にエラー;
  • [ ] workspace はテナント/タスクごとの独立ディレクトリで、/ やホームディレクトリではない;
  • [ ] mode のデフォルトは INTERACTIVE;
  • [ ] engine.setApprover() を実装済みで、タイムアウト/切断時のフォールバックがある;
  • [ ] setQuestionHandler() / setPlanHandler() / setDirectoryHandler() を実装済み(対応ツールを使う場合);
  • [ ] maxIterations、contextWindow、capTokens に明確な上限がある;
  • [ ] toolResultDir をテナントごとに分離。

セキュリティ

  • [ ] shell ポリシーとサンドボックス状態が期待どおり(engine.permissions.sandbox);
  • [ ] コマンド許可リストにワイルドカード/複合コマンドを含めない;
  • [ ] MCP ツールは承認を維持し、設定ファイルに平文の鍵を含めない(${ENV} を使用);
  • [ ] 添付ファイルのサイズ/数量制限を外層でもう一度検証済み;
  • [ ] 監査ログの保持期間とマスキング方針を決定済み。

信頼性

  • [ ] 同一セッションの run() を直列化済み;
  • [ ] クライアント切断 → requestInterrupt();
  • [ ] プロセス終了 → mcpManager.closeAll()、hook.saveState();
  • [ ] 再起動復元 → SavedState.messages を読み、権限モード/許可リストを復元;
  • [ ] error.retriable に対して「リトライ」入口を提供;
  • [ ] turn_end.status の分布と token 使用量を監視。

パフォーマンス

  • [ ] 単一セッションのイベントを無限にバッファしない(SSE は直接転送し、メモリ上の大きな配列に落とさない);
  • [ ] logDir を日付で分割し、定期的にクリーンアップ;
  • [ ] 大きなファイルのツール結果は保存予算を通し、全体をそのままモデルに戻さない。

20. API クイックリファレンス ​

20.1 メインエントリのエクスポート(抜粋) ​

ts
// 引擎
TurnEngine
createDefaultEngine
EventType, ApprovalOutcome
Mode, PermissionEngine
ToolRegistry, RiskClass
OpenAIProvider

// 工具注册
registerFileTools, registerReadOnlyFileTools, registerShellTool, registerSearchTool,
registerWebTools, registerGitTools, registerDiffTools, registerTodoTool, TodoList,
registerExploreTool, registerMediaTool, registerTerminalTools,
registerAskTool, setQuestionHandler,
registerPlanTool, setPlanHandler,
registerDirectoryTool, setDirectoryHandler

// 钩子与视图
ConsoleHook, SubagentHook, CompositeHook, FileLogHook,
MarkdownRenderer, approvalFields, approvalToHtmlTable, approvalToMarkdownTable

// 提示词
buildInstructions, buildEnvBlock, getPrompts, promptSets, PROMPT_LANGS

// Agent / MCP
AgentRegistry, BUILTIN_AGENTS
McpClient, McpManager, MCP_SEARCH_TOOL, MCP_CALL_TOOL,
loadMcpConfigFile, saveMcpConfigFile, readMcpConfigFile,
normalizeMcpConfig, normalizeMcpConfigs, defaultMcpConfigPath,
withBuiltinMcpConfigs, splitCommandLine, BUILTIN_MCP_CONFIGS

// 附件
MessageAttachment, StoredAttachment,
normalizeAttachments, saveAttachments, readStoredAttachment, stripAttachments,
MAX_ATTACHMENTS, MAX_ATTACHMENT_BYTES, ATTACHMENT_DIR, AttachmentError

// 沙箱
resolveSandbox, wrapWithSandbox, describeSandbox, buildMacProfile, buildBwrapArgs

// VCS
SessionVcs, VcsHook, resolveVcsPlan, agentRef, sessionIndexPath

// 会话命名:TurnEngine 内部使用 genSessionNamePrompt;
// 纯逻辑实现(shouldAutoName / sanitizeSessionName)不在主入口,
// 需要自定义命名时参考 packages/core/src/engine/sessionNaming.ts(非稳定 API)

20.2 よく使う環境変数 ​

変数作用
OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_MODELProvider 設定
OPENAI_REASONING_EFFORT推論の強度
OPENAI_FORCE_API_MODEresponses / chat
OPENAI_MAX_RETRIES / OPENAI_TIMEOUT_MS / OPENAI_STREAM_IDLE_MSリクエスト信頼性
SOCKS5_PROXYSOCKS5 プロキシ
MAX_ITERATIONSサーバー側組み立て時のイテレーション上限
CONTEXT_WINDOW / COMPACTION_THRESHOLD_PCT / COMPACTION_SOFT_THRESHOLD_PCT / COMPACTION_CAP_TOKENSサーバー側組み立て時の圧縮パラメータ
COMPACTION_MODEL / COMPACTION_REASONING_EFFORT / COMPACTION_ELIDE / COMPACTION_DEDUP圧縮挙動
NETWORK_RETRY_MAX / NETWORK_RETRY_BASE_MS一時的なネットワークエラーのリトライ
TOOL_RESULT_MAX_CHARS / TOOL_RESULT_SAVE_THRESHOLD / TOOL_RESULT_DIRツール結果の予算
EXPLORE_MAX_ITERATIONSサブエージェントのイテレーション上限(デフォルト 30)
MCP_CALL_TIMEOUT_MSMCP ツール呼び出しのタイムアウト(デフォルト 300s)
ZQAGENT_MCP_LAZYMCP 遅延読み込みスイッチ

21. トラブルシューティング FAQ ​

Q:ツールが実行されず、モデルが「ユーザーが拒否した」と言う。 approver が DENY を返したか、defaultApproval: "deny" が設定されています。approver のログを確認してください。

Q:「ツール X は承認が必要ですが、エンジン組み立て時に approver が注入されていません」というエラー。engine.setApprover() を呼び出していません。これはデフォルト defaultApproval: "error" の想定された挙動です。 このエラーは approver が投げるもので、for await から直接出てくるため、呼び出し側で try/catch が必要です(または deny に変更する / 実際の approver を提供する)。

Q:「モデルが指定されていません」というエラー。model を渡すか、OPENAI_MODEL を設定してください。エンジンがモデルを代わりに選択することはありません。

Q:「OPENAI_API_KEY が設定されていません」というエラー。 カスタム provider を渡すか、OPENAI_API_KEY を設定してください。

Q:run() がずっと終わらない。 イベントストリームを最後まで消費したか確認してください。ツールが詰まっていないか確認してください(run_shell の長いタスクはバックグラウンドに置くべきです)。 provider が opts.signal を無視していないか確認してください。

Q:「停止」をクリックしても反応がない。 イベントストリームの読み取りを停止するのではなく、engine.requestInterrupt() を呼び出す必要があります。カスタム provider は opts.signal に応答しなければなりません。

Q:再起動後にユーザーの以前の「常に許可」が失われた。SavedState に permissionMode / allowTools / allowCommands があるため、復元時に 手動で engine.permissions に書き戻す必要があります。

Q:同じワークスペースの複数セッションがファイルを相互上書きする。 各セッションは独立した engine ですが、ファイルシステムは共有です。ConflictGuard(組み込み)を使うと 「読み取り後に他者に変更された」ことを検出できます。完全に分離するにはセッションごとに独立した workspace を与えるか、vcs 監査を有効化してください。

Q:MCP ツールが現れない。connectAll() は非同期なので、完了を待つ必要があります。ツール表は各 LLM 呼び出し時にリアルタイムで読み取られるため、 接続完了後の次のターンで現れます。failed リストと設定パスを確認してください。

Q:CommonJS プロジェクトで require() が ERR_PACKAGE_PATH_NOT_EXPORTED を報告する。 0.2.60 以降を使用してください。それ以前の CJS 成果物は ESM-only 依存関係により読み込みに失敗します。

Q:モデルリクエストをプロキシ経由にしたい。SOCKS5_PROXY=socks5://host:port を設定してください。HTTP/HTTPS プロキシは現時点では未対応です。


22. バージョン、ビルド、リリース ​

  • 現在のパッケージバージョン:0.3.25(packages/core/package.json)。

  • ライセンス:Apache-2.0。

  • ビルド:

    bash
    pnpm --filter @zqagent/core build     # tsup → packages/core/dist(ESM + CJS + d.ts)
    pnpm --filter @zqagent/core typecheck
    pnpm --filter @zqagent/core test      # Vitest
  • リリース:bash publish_npm.sh [patch|minor|major](core のみ)、またはリポジトリレベルの scripts/publish-all-npm.sh(shared → core → server → cli)。

  • 成果物形態:dist/index.js(ESM)、dist/index.cjs(CJS)、dist/index.d.ts、 dist/index.d.cts。@zqagent/shared は外部依存として一緒にインストールされます。

アップグレードの推奨:

  • 正確なバージョンを固定し、アップグレード前に統合テストを一通り実行;
  • TurnEngineOpts / CreateEngineOpts / イベントフィールドの変更に注意(追加フィールドは通常互換、 削除/改名はメジャーバージョンで説明);
  • internal/* を安定 API として扱わない。アップグレード時に変更される可能性があります。

関連ドキュメント

Last updated: