本文はリポジトリ
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、統合モード、本番運用上の注意点を扱います。
- エンジン実装:
packages/core/src/engine/TurnEngine.ts - パッケージ入口と全エクスポート:
packages/core/src/index.ts - パッケージ設定(
exports/ 依存関係 / ビルド):packages/core/package.json - リポジトリ全体のアーキテクチャ:architecture.md
- 内部サーバー側の参考実装:
packages/server/src/session.ts
目次
- それが何であり、何ではないか
- アーキテクチャ上の位置づけとパッケージ関係
- インストールと実行環境
- 5分で動かす
- 中核概念
- エンジンの組み立て
- ツール:組み込みとカスタム
- 権限、承認、セキュリティ境界
- イベントストリーム完全リファレンス
- モデル Provider
- ライフサイクルフックと永続化
- 1ターン Turn の制御
- 添付ファイル
- コンテキスト圧縮
- MCP 統合
- サブエージェントと Agent レジストリ
- 変更追跡とロールバック
- 4つの統合モードとコード骨子
- 本番チェックリスト
- API クイックリファレンス
- トラブルシューティング FAQ
- バージョン、ビルド、リリース
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/core | Agent エンジン(本文の主役) | はい |
@zqagent/shared | core と 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) |
| OS | Linux / macOS / Windows。shell ツールは Windows で利用可能な shell を自動選択 |
3.2 インストール
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 条件付きでエクスポートします。利用側の推奨設定:
{
"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/ 内部のファイルを参照しないでください:
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+):
// 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;
}
}実行前に設定:
export OPENAI_API_KEY=sk-...
export OPENAI_MODEL=gpt-4o-mini # 或任意 OpenAI 兼容模型
node agent-demo.mjsこの1つのスクリプトで、組み込み時に最も重要な4つ——組み立て、承認、イベント消費、マルチターンコンテキスト (同じ engine でもう一度 run() を呼ぶと前ターンの履歴が引き継がれる)——をすでにカバーしています。
5. 中核概念
以下の6つのオブジェクトを理解すれば、以降の API がすべて対応づけられます。
| 概念 | 型 | 責務 |
|---|---|---|
| Engine | TurnEngine | 1セッションのランタイム。メッセージ履歴、provider、ツール表、権限エンジンを保持 |
| Turn | engine.run() の1回の呼び出し | ユーザーの1入力 → 複数回の LLM/ツール反復 → 1つの turn_end |
| Iteration | 内部ループ | 1回の LLM 呼び出し + その応答が要求したすべてのツール実行 |
| Event | WsEvent | エンジンが外部に出力する唯一のリアルタイムチャネル(非同期ジェネレータ) |
| Tool | ToolSpec | モデルが呼び出せる関数 + JSON Schema + リスクメタデータ |
| Approver | Approver | 権限エンジンが「人間への確認が必要」と判断した際に、呼び出し側が許可/拒否を決める |
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
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 参照):
providerが渡されていない場合new OpenAIProvider({ logDir })を作成;- サンドボックス方案
resolveSandbox()を解決; ToolRegistryとConflictGuardを作成し、21個の組み込みツールを登録(第7章参照);PermissionEngineを作成(ワークスペース、モード、コマンド許可リスト、shell ポリシー、サンドボックス状態);instructionsが渡されていない場合buildInstructions(agentType, workspace, today, lang)で システムプロンプトを生成;TurnEngineを組み立て;vcsが有効ならCompositeHookでVcsHookを並列接続;defaultApprovalに従ってフォールバック approver を注入。
CreateEngineOpts 全パラメータ
| パラメータ | 型 / デフォルト値 | 説明 |
|---|---|---|
workspace | string(必須) | ワークスペースルート = ファイルとコマンドの権限境界 |
model | string、省略時は OPENAI_MODEL を読む | モデル識別子。いずれも空の場合 run() が明確にエラーを出し、モデルをフォールバックしない |
provider | ProviderClient | カスタム provider。省略時は OpenAIProvider を作成 |
mode | Mode.INTERACTIVE | DISCUSS / PLAN / INTERACTIVE / AUTO |
maxIterations | DEFAULT_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章参照 |
instructions | buildInstructions により生成 | システムプロンプトを上書き |
agentType | "code" | code(プログラミング助手)/ cowork(汎用コラボレーション)、デフォルトプロンプトに影響 |
lang | 簡体字中国語 | zh / en / zh-TW / ja / ko / es、プロンプト、命名、要約の言語に影響 |
messages | [] | 初期会話履歴(セッション復元時に渡す) |
hook | なし | ライフサイクルフック、第11章参照 |
subagentHookFactory | なし | explore 呼び出しごとにサブエージェント hook を生成 |
contextWindow | 自動検出、フォールバック 128K | モデルのコンテキストウィンドウ(token) |
thresholdPct | 0.8 | ハード圧縮しきい値(ウィンドウに対する割合) |
softThresholdPct | ハードしきい値の75% | ソフトしきい値:古いツール結果のみを淘汰し、LLM は呼び出さない |
capTokens | null | ハードしきい値の絶対上限(token)、null = 制限なし |
toolResultDir | 状態ディレクトリ下の tool-results | 予算超過したツール結果の保存先。マルチテナントではテナントごとに分離必須 |
logDir | 記録しない | LLM リクエスト監査ログのルートディレクトリ(日付別ディレクトリ) |
defaultApproval | "error" | approver 未注入時:error / deny / allow |
lang はデフォルトプロンプト、セッション命名、要約の言語にのみ影響します。明示的に instructions を渡した場合はそれが優先されます。
6.2 経路 B:手動で TurnEngine を組み立てる
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 の公開属性とメソッド:
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 |
| Shell | run_shell, shell_task_output, shell_task_kill | 実行 | registerShellTool |
| 検索 | grep | 読み取り専用 | registerSearchTool |
| ネットワーク | web_search, web_fetch | 外部 | registerWebTools |
| Git | git_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)に依存するため、自分で注入する必要があります:tsimport { 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):
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;例:
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 読み取り専用ツールセット
サブエージェントや読み取り専用シーン用:
import { registerReadOnlyFileTools } from "@zqagent/core";
registerReadOnlyFileTools(registry, workspace); // read_file / list_files8. 権限、承認、セキュリティ境界
8.1 判断フロー
PermissionEngine.evaluate() は固定順序で判断します (packages/core/src/permissions/PermissionEngine.ts:173):
- 読み取り専用モード(
DISCUSS/PLAN)→ 副作用のある操作は直接拒否; - ファイル書き込みパスが
workspaceRoot配下にない → 直接拒否; - 読み取り専用リスク(
RiskClass.READ)→ 直接許可; AUTOモード → 副作用のある操作をすべて許可;- shell 承認免除ポリシー(
shellPolicy+ サンドボックス)→ 直接許可される場合がある; - コマンド許可リスト / セッションコマンド許可リスト → 直接許可;
- セッションツール許可リスト(ユーザーが「このツールを常に許可」を選択済み)→ 直接許可;
- その他 →
needsUser: true、approver に委ねる。
戻り値の構造:
interface Decision {
allowed: boolean;
reason: string; // 展示给用户 / 审批卡片
needsUser: boolean; // true = 需要审批
}8.2 実行モード
enum Mode {
DISCUSS = "discuss", // 只读,禁止写入与执行
PLAN = "plan", // 只读,禁止写入与执行
INTERACTIVE = "interactive", // 有副作用操作逐次审批(推荐默认)
AUTO = "auto", // 有副作用操作全部放行
}interactive はデフォルトであり、実際のユーザー向けに最も安全なモードです。auto は隔離環境、破棄可能なワークスペース、または完全に信頼できる入力でのみ使用してください。
8.3 承認コールバック
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(デフォルト) | 許可リスト以外は一律承認 |
sandbox | OS レベルのサンドボックスが実際に有効な場合のみ承認免除、それ以外は承認 |
workspace | サンドボックス有効 → 承認免除;無効 → ワークスペースパス判定でフォールバック |
all | shell をすべて承認免除(高リスク、shell にのみ効く auto と同等) |
サンドボックス設定:
createDefaultEngine({
workspace,
sandbox: {
mode: "auto", // auto / off / required
extraWritable: ["/data/cache"], // 额外可写路径
},
});- macOS:
sandbox-execでfile-write*をワークスペース + 一時ディレクトリに限定; - Linux:
bwrapでシステム全体を読み取り専用マウント + ワークスペース書き込み可能; - Windows:同等のプロセスレベルファイルシステムサンドボックスはなく、
enforced: falseとなりパスヒューリスティックに退化します(これは煩わしさを減らす戦略であり、セキュリティ境界ではありません)。
required モードではサンドボックスを検出できない場合に明確にエラーを出します。「必ず隔離する」ことをコンプライアンス要件とするデプロイに適しています。
8.5 コマンド許可リスト
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 構造
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_start | taskId, input, attachmentCount?, attachments? | 各ターン開始 |
assistant_delta | text, reqId, taskId | モデルのストリーミングテキスト増分 |
reasoning_delta | text, reqId, taskId | 推論過程の増分(一部モデル) |
assistant_message | text, reasoning, tool_calls(ツール名配列), usage?, reqId, taskId, logTime | 1回の LLM 応答終了 |
tool_proposed | reqId(=ツール呼び出し ID), name, arguments, taskId | モデルがツール呼び出しを提案 |
permission_required | taskId, reqId, name, arguments, reason, category | 承認が必要(その後 approver を呼び出す) |
tool_started | taskId, reqId, name, startTime? | ツール実行開始 |
tool_finished | taskId, reqId, name, status, reason?, duration?, result_preview? | ツール終了 |
iteration_end | iteration, taskId | 1イテレーション(LLM + ツール)終了 |
turn_end | status, iterations, produced_files, taskId | 1ターン終了(必ず出力) |
error | error, error_type, retriable, iteration, reqId, logTime, taskId, stack? | エンジンエラー |
interrupted | iterations, reqId, taskId | ユーザー中断 |
compacting | taskId, trigger, tokens, estimated, contextWindow, thresholdPct, thresholdTokens, outboundMessages | 圧縮開始 |
compacted | reqId, taskId, method, messagesBefore, messagesAfter, boundaryIndex?, model?, summaryPreview?, keptUserMessages?, droppedUserMessages? | 圧縮完了 |
queue_steering | text, taskId, attachments? | キューされた補充指令が注入された |
llm_request | reqId, taskId, logTime, model, msgCount, toolCount, compact? | 各 LLM リクエスト前(監査用) |
ask_user_question | taskId, question, options, allowText, multi | モデルが ask_user を呼び出した |
plan_proposed | plan | モデルが propose_plan を呼び出した |
directory_requested | reason, path, writable | モデルが request_directory を呼び出した |
llm_retry | attempt, max, reason, waitMs, iteration, reqId | 一時的なネットワークエラーで自動リトライ |
session_named | name, source | 列挙は予約済み。現在は CLI/server が命名後に自らブロードキャストし、TurnEngine は出力しない |
tool_finished.status の値:
| status | 意味 |
|---|---|
ok | 実行成功 |
error | ツールが例外を投げた、またはエラーを返した |
denied | 権限拒否またはユーザー拒否 |
interrupted | 実行中に中断 |
turn_end.status の値:
| status | 意味 |
|---|---|
completed | モデルがツールを要求しなくなり、正常終了 |
max_iterations_exceeded | maxIterations に到達 |
interrupted | ユーザー中断 |
error | LLM/エンジンエラーによる終了 |
9.3 消費例:SSE に変換
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
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_KEY | API Key |
OPENAI_BASE_URL | 互換エンドポイント |
OPENAI_MODEL | デフォルトモデル |
OPENAI_REASONING_EFFORT | low / medium / high |
OPENAI_FORCE_API_MODE | responses / chat |
OPENAI_MAX_RETRIES | SDK リトライ回数 |
OPENAI_TIMEOUT_MS | リクエストタイムアウト |
OPENAI_STREAM_IDLE_MS | ストリームアイドルウォッチドッグ |
SOCKS5_PROXY | LLM とネットワークツールが SOCKS5 プロキシを使用 |
10.2 カスタム Provider
エンジンは2つのメソッドにのみ依存します(packages/core/src/providers/OpenAIProvider.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:
interface StreamChunk {
content: string | null;
reasoning: string | null;
toolCalls: ToolCall[] | null; // 仅流结束 chunk 给完整列表
usage: TokenUsage | null;
finishReason: string | null;
}カスタム provider 実装時の最重要ポイント2つ:
- 最後の chunk には
toolCallsまたはfinishReasonが必須。そうでないとエンジンはストリームが正常終了していないと判断します; opts.signalの abort を受けたら直ちに読み取りを停止し、例外を投げる/ストリームを終了する。そうでないと「停止」ボタンが無効になります。
chat() は主にセッション命名などの軽量リクエストに使われます。null を返しても主フローには影響しません。
10.3 推論の強度
createDefaultEngine({
workspace,
reasoningEffort: "high", // low / medium / high
});provider に透過されます。OpenAI 系モデルでは有効で、DeepSeek は自動スキップされます(thinking スイッチを使い、レベル制御には対応していません)。
11. ライフサイクルフックと永続化
11.1 EngineHook の全コールバック
| コールバック | 引数 | 用途 |
|---|---|---|
onTurnStart | input | 1ターン開始 |
onTurnEnd | status, iterations, producedFiles | 1ターン終了 |
onAssistantDelta | text | ストリーミングテキスト |
onReasoningDelta | text | ストリーミング推論 |
onAssistantMessage | text, toolCalls, usage? | 完全な応答(課金でよく使用) |
onToolProposed | name, args | ツール提案 |
onToolStarted | name | ツール開始 |
onToolFinished | name, status, resultPreview | ツール終了 |
onPermissionRequired | name, reason | 承認が必要 |
onIterationEnd | iteration | イテレーション終了(チェックポイントでよく使用) |
onCompacting / onCompacted | info / text, info | 圧縮 |
onError | error, errorType, info? | エラー(retriable を含む) |
onRetry | attempt, max, reason | ネットワーク自動リトライ |
onInterrupted | iterations | 中断 |
onEvent | type, data | 未マッチイベントのフォールバック |
フックは同期コールバックであり、エンジンは await しません。したがって:
- hook 内で長時間ブロックする操作をしない(エンジンが詰まります);
- hook が投げた例外はエンジンに飲み込まれ(
_emitHook内部の try/catch)、主フローに影響しません; - バックプレッシャー/順序保証が必要なデータは、hook ではなく
run()のイベントストリームを消費してください。
11.2 複数 Hook の組み合わせ
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:ファイルレベルの永続化と復元
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 のフィールド:
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 マルチターン会話
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 中断
// 在另一个地方(例如 HTTP DELETE / 用户点「停止」)
engine.requestInterrupt();これは次を行います:
- キャンセルフラグを立てる;
- 実行中の LLM ストリームを abort;
- 実行中のツールを abort(
run_shellは子プロセスツリーを kill);
その後、イベントストリームは interrupted + turn_end(status: "interrupted") を出力します。 「イベントストリームの消費停止」を中断の代わりにしないでください。その場合エンジンは進行せず、状態もクリーンアップされません。
12.3 リトライ
for await (const e of engine.retry()) {
// 仅在上一轮以可重试错误结束时才会产出事件
}12.4 一時停止した承認からの復元
engine.resume() は「履歴末尾に未回答のツール呼び出しがある」シーンに使います(典型的にはサービス再起動後に一時停止した承認を復元する場合):
for await (const e of engine.resume()) {
// 重新处理挂起的 tool_calls;approver 会按已 resolve 的结果立即返回
}一時停止中のツール呼び出しがない場合、resume() は直接戻り、イベントを出力しません。
12.5 Steering:実行中の追加入力
engine.queueSteering("顺便把测试也跑一下");
engine.queueSteering("忽略 dist 目录", attachments, refs);エンジンはイテレーションの合間にキューされた指令を user メッセージとして注入し、queue_steering を出力します。 「Agent が長いタスクを実行中に、ユーザーが一言補足したい」というインタラクションに適しています。まだ注入されていない指令は取り消せます:
engine.removeSteering("顺便把测试也跑一下"); // 已注入返回 false12.6 並行性の制約
- 同じ engine で同時に実行できるターンは1つだけ;
- ツールレベル:低リスクツールは並行実行され、高リスク/承認要/インタラクション系ツールは直列実行される;
- サービス層では同一セッションの
run()をキュー化し、2つのリクエストが同時にmessagesを書き込むのを避けるべきです。
13. 添付ファイル
13.1 データ構造
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_ATTACHMENTS | 4 | 1回あたり最大添付数 |
MAX_ATTACHMENT_BYTES | 10 MB | 単一ファイルかつ合計上限 |
ATTACHMENT_DIR | "attachments" | セッションディレクトリ下のサブディレクトリ |
13.2 推奨フロー:先に保存し、参照を渡す
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/ にのみ存在します。読み取り時は次を使います:
const { name, mimeType, size, base64 } = readStoredAttachment(sessionDir, ref.path);セキュリティ上の注意:
readStoredAttachmentはattachments/<file>形式のみ受け付け、..とサブディレクトリを拒否します;- 添付ファイルは LLM リクエストに入り、
logDir監査ログはメッセージを記録します(base64 を記録するかは provider のログ実装次第であり、本番環境では評価すべきです); - ユーザーがアップロードした任意の HTML/スクリプトをテキスト添付として直接レンダリングしないでください。
14. コンテキスト圧縮
長いセッションをモデルウィンドウに無限に詰め込むことはできないため、エンジンは自動的に圧縮します。
14.1 トリガー機構
| 概念 | デフォルト | 説明 |
|---|---|---|
| コンテキストウィンドウ | 自動検出、フォールバック 128K | contextWindow で上書き可能 |
| ハードしきい値 | ウィンドウの80% | thresholdPct |
| ソフトしきい値 | ハードしきい値の75%(≈60% ウィンドウ) | softThresholdPct、古いツール結果のみ淘汰 |
| 絶対上限 | なし | capTokens |
| 最近保持 | 約25% | 圧縮後に保持する直近会話の割合 |
| 要約出力上限 | 3000 token | 内部固定 |
フロー:
- ソフトしきい値到達:古いツール結果を淘汰し、重複する
read_fileを去重し、LLM を呼び出さない; - ハードしきい値到達:LLM を呼び出して要約を生成し、
compacting/compactedを出力; - API がコンテキストオーバーフローを報告:強制圧縮後にリトライ。
14.2 イベントとフック
// engine.run(...) 的事件流中会出现:
// { type: "compacting", data: { trigger: "threshold", tokens, contextWindow, ... } }
// { type: "compacted", data: { method: "summarized" | "trimmed", messagesBefore, messagesAfter, ... } }onCompacting / onCompacted フックは同じ情報を受け取ります。UI はこれに基づいて「コンテキストを圧縮中」と表示できます。
14.3 調整可能パラメータと環境変数
createDefaultEngine({
workspace,
contextWindow: 200_000,
thresholdPct: 0.85,
softThresholdPct: 0.7,
capTokens: 120_000,
});| 環境変数 | 作用 |
|---|---|
COMPACTION_MODEL | 要約に使用するモデル(省略時は現在のモデルを継続使用) |
COMPACTION_REASONING_EFFORT | 要約の推論強度(デフォルト low) |
COMPACTION_ELIDE | 0 で古いツール結果の淘汰を無効化 |
COMPACTION_DEDUP | 0 で重複読み取りの去重を無効化 |
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 最小構成
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 は複数の記法をサポートします:
// 裸数组
[
{ "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 遅延読み込み(ツールが多い場合)
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 ライフサイクル
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ターン実行し、レポートを出力する; - メインエージェントは最終レポートのみを受け取り、サブエージェントの中間過程はメインコンテキストに入らない。
サブエージェントの挙動をカスタマイズ:
createDefaultEngine({
workspace,
subagentHookFactory: () => new SubagentHook("explore"), // 每个子代理一个实例
});subagentHookFactory を渡さない場合、サブエージェントは完全にサイレントになります。SubagentHook はサブエージェントのツール呼び出し をインデントしてターミナルに出力する CLI の参考実装です。Web シーンでは独自の hook を実装し、サブエージェントイベント をフロントエンドに転送できます(注意:サブエージェントイベントはメインの run() のイベントストリームを通りません)。
手動登録:
registerExploreTool(registry, {
workspace,
provider,
model,
lang: "zh",
hookFactory: () => mySubagentHook,
toolResultDir,
});16.2 AgentRegistry
AgentRegistry はメモリ内のロール表であり、「名前 + 説明 + システムプロンプト + 任意のモデル」を管理します:
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 有効化
const engine = createDefaultEngine({
workspace,
vcs: { mode: "auto", sessionId: "tenant-42-session-7" },
});mode:
| 値 | 挙動 |
|---|---|
off(デフォルト) | 無効 |
auto | Git リポジトリならネイティブモード、そうでなければ shadow リポジトリ |
native | ワークスペースの .git を使うが、独立した index と refs/agent/* を使用し、ユーザーの HEAD/staging には触れない |
shadow | <workspace>/.agent/vcs にシャドウリポジトリを作成 |
native / shadow はどちらもローカルに git が必要です。見つからない場合は off に退化します(書き込み競合検出は引き続き利用可能)。
17.2 照会とロールバック
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()。
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
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 メインエントリのエクスポート(抜粋)
// 引擎
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_MODEL | Provider 設定 |
OPENAI_REASONING_EFFORT | 推論の強度 |
OPENAI_FORCE_API_MODE | responses / chat |
OPENAI_MAX_RETRIES / OPENAI_TIMEOUT_MS / OPENAI_STREAM_IDLE_MS | リクエスト信頼性 |
SOCKS5_PROXY | SOCKS5 プロキシ |
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_MS | MCP ツール呼び出しのタイムアウト(デフォルト 300s) |
ZQAGENT_MCP_LAZY | MCP 遅延読み込みスイッチ |
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。
ビルド:
bashpnpm --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 として扱わない。アップグレード時に変更される可能性があります。
関連ドキュメント
