Este artículo se sincroniza desde el repositorio
docs/core-integration.md. Los enlaces al código fuente apuntan al repositorio de GitHub; las páginas relacionadas legibles en el sitio se han cambiado a enlaces internos.
Integrar @zqagent/core en sistemas de terceros: manual completo de integración
Chino simplificado · Corresponde al repositorio actual 0.3.25
Este artículo está dirigido a equipos que quieran incorporar capacidades de AI Agent en sus propios sistemas: tu sistema puede ser un servicio web, una aplicación de escritorio, un Worker de cola de tareas, o un producto que ya cuenta con un sistema de sesiones/usuarios/facturación. El objetivo es no instalar la CLI, no iniciar el server de zqagent, y usar únicamente @zqagent/core como biblioteca para obtener todo el conjunto de capacidades Agent: «modelo + herramientas + permisos + flujo de eventos + persistencia».
¿Solo quieres ejecutar primero un ejemplo mínimo? Consulta embedding.md y
examples/embed-basic. Este artículo es su versión completa y cubre toda la API pública, los patrones de integración y las consideraciones para producción.
- Implementación del motor:
packages/core/src/engine/TurnEngine.ts - Punto de entrada del paquete y todas las exportaciones:
packages/core/src/index.ts - Configuración del paquete (
exports/ dependencias / build):packages/core/package.json - Descripción general de la arquitectura del repositorio: architecture.md
- Implementación de referencia del servidor interno:
packages/server/src/session.ts
Índice
- Qué es y qué no es
- Posicionamiento arquitectónico y relación entre paquetes
- Instalación y entorno de ejecución
- Poner en marcha en cinco minutos
- Conceptos centrales
- Montaje del motor
- Herramientas: integradas y personalizadas
- Permisos, aprobación y límites de seguridad
- Referencia completa del flujo de eventos
- Proveedor de modelos
- Hooks de ciclo de vida y persistencia
- Control de un turno
- Adjuntos
- Compactación de contexto
- Integración con MCP
- Subagentes y registro de Agent
- Seguimiento de cambios y reversión
- Cuatro patrones de integración y esqueletos de código
- Lista de verificación para producción
- Referencia rápida de API
- FAQ de solución de problemas
- Versión, build y publicación
1. Qué es y qué no es
@zqagent/core es una biblioteca de motor Agent. Se encarga de:
- mantener el historial de mensajes de una sesión;
- invocar al LLM (en streaming) y analizar el texto, el proceso de razonamiento y las llamadas a herramientas;
- ejecutar herramientas mediante
ToolRegistry; - determinar con
PermissionEnginesi cada llamada a herramienta debe permitirse, denegarse o preguntar a una persona; - exponer todo el proceso al invocador como un flujo de eventos mediante generador asíncrono;
- compactar el historial cuando se excede el contexto;
- opcional: persistencia de eventos y checkpoints, integración con MCP, subagentes y snapshots de Git.
No se encarga de:
- UI / renderizado de terminal (
ConsoleHookes solo una implementación de referencia); - servicios HTTP / WebSocket / SSE (
@zqagent/serveres una implementación de referencia); - usuarios, inquilinos, facturación y autenticación;
- indexación de sesiones y base de datos (
FileLogHookes una implementación de referencia a nivel de archivos); - tomar decisiones de aprobación en nombre del invocador.
En una frase: tú te encargas de la capa de producto y del ciclo de vida de la sesión; el motor se encarga de cómo se ejecuta un turno de conversación.
2. Posicionamiento arquitectónico y relación entre paquetes
┌─────────────────────────────┐
你的系统 ──────▶│ @zqagent/core │
(Web/桌面/Worker)│ TurnEngine / ToolRegistry │
│ PermissionEngine / Provider │
│ 内置工具 / MCP / 压缩 / VCS │
└──────────────┬──────────────┘
│ 依赖
▼
┌─────────────────────────────┐
│ @zqagent/shared │
│ 模型能力表 / 协议 / 加密等 │
└─────────────────────────────┘
仓库自带的两个「完整应用」,可作为参考实现:
@zqagent/server = 多租户会话 + HTTP/SSE + 挂起式审批
@zqagent/cli = 终端渲染 + 会话持久化 + 交互审批La dirección de dependencias es unidireccional: shared ← core. core no conoce la existencia de server / cli ni depende de ellos en sentido inverso. Cuando un sistema de terceros incorpora core, no arrastra consigo el servicio HTTP, Tauri, la UI de consola, etc.
Paquetes del repositorio:
| Paquete | Propósito | ¿Se puede usar de forma independiente? |
|---|---|---|
@zqagent/core | Motor Agent (protagonista de este artículo) | Sí |
@zqagent/shared | Módulos base compartidos por core y hub | Se instala automáticamente como dependencia de core |
@zqagent/server | Implementación de referencia de servicio HTTP multiinquilino | Sí, pero pertenece a la capa de aplicación |
@zqagent/cli | Implementación de referencia de aplicación de terminal | Sí, pero pertenece a la capa de aplicación |
3. Instalación y entorno de ejecución
3.1 Requisitos del entorno
| Elemento | Requisito |
|---|---|
| Node.js | >= 18.17 (engines de package.json) |
| Sistema de módulos | Doble artefacto ESM y CommonJS; tanto import como require funcionan |
| Tipos | Las declaraciones de tipos se publican con el paquete (dist/index.d.ts / .d.cts) |
| Sistema operativo | Linux / macOS / Windows; la herramienta shell selecciona automáticamente un shell disponible en Windows |
3.2 Instalación
npm i @zqagent/core
# 或
pnpm add @zqagent/core
# 或
yarn add @zqagent/coreVerificación de versión: este artículo corresponde al repositorio
0.3.25. La versión disponible en npm se determina connpm view @zqagent/core version; si la versión de npm está por detrás del repositorio, se puede ejecutarpnpm --filter @zqagent/core builddentro del repositorio y luego referenciarlo mediantefile:/ workspace. Se recomienda fijar la versión exacta enpackage.jsonpara evitar que el comportamiento del motor cambie con versiones menores.
3.3 Configuración de TypeScript
El paquete usa resolución de módulos node16 y exportaciones con la condición types. Configuración recomendada para el consumidor:
{
"compilerOptions": {
"target": "es2022",
"module": "node16",
"moduleResolution": "node16",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}Si el proyecto es ESM puro ("type": "module"), usa import directamente; si es CommonJS, require("@zqagent/core") también funciona, pero requiere la versión 0.2.60 o superior (los artefactos CJS de versiones anteriores fallan al cargarse debido a dependencias solo ESM).
3.4 Importación por subrutas
La sección exports de packages/core/package.json es cerrada; solo están disponibles las siguientes subrutas. Cuando necesites importar implementaciones de herramientas, usa las subrutas; no referencies archivos internos de 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";Las subrutas internal/* (internal/engine/FileLogHook, internal/mcp/config, etc.) son API no estable para uso del server / CLI del mismo repositorio y los terceros no deberían depender de ellas; este artículo solo usa el punto de entrada principal y las subrutas estables anteriores. FileLogHook, SavedState, las funciones de configuración de MCP, etc. también se exportan en realidad desde el punto de entrada principal; prioriza el punto de entrada principal.
4. Poner en marcha en cinco minutos
A continuación se muestra un script ESM completo y ejecutable (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;
}
}Configura antes de ejecutar:
export OPENAI_API_KEY=sk-...
export OPENAI_MODEL=gpt-4o-mini # 或任意 OpenAI 兼容模型
node agent-demo.mjsEste único script ya cubre las cuatro cosas más importantes al integrar: montaje, aprobación, consumo de eventos y contexto multi-turno (llamar de nuevo a run() sobre el mismo engine incluirá el historial del turno anterior).
5. Conceptos centrales
Entendiendo los seis objetos siguientes, el resto de la API encajará.
| Concepto | Tipo | Responsabilidad |
|---|---|---|
| Engine | TurnEngine | Runtime de una sesión. Contiene el historial de mensajes, el provider, la tabla de herramientas y el motor de permisos |
| Turn | Una llamada a engine.run() | Una entrada del usuario → varias iteraciones LLM/herramientas → un turn_end |
| Iteration | Bucle interno | Una llamada al LLM + toda la ejecución de herramientas solicitada por esa respuesta |
| Event | WsEvent | El único canal en tiempo real de salida del motor (generador asíncrono) |
| Tool | ToolSpec | Función invocable por el modelo + JSON Schema + metadatos de riesgo |
| Approver | Approver | Cuando el motor de permisos determina que «hay que preguntar a una persona», el invocador decide permitir/denegar |
5.1 La sesión y el Engine tienen una relación uno a uno
Crea un TurnEngine por cada sesión; no compartas el mismo engine entre varias sesiones:
engine.messageses el historial de la sesión y es intrínsecamente secuencial;- el «permitir siempre en esta sesión» dentro de
engine.permissionses estado a nivel de sesión; engine.compactionStatees el estado de compactación a nivel de sesión;- ejecutar dos turnos de
run()simultáneamente en el mismo engine contamina el historial mutuamente.
La forma correcta de manejar múltiples sesiones es Map<sessionId, TurnEngine>, una instancia por sesión; consulta capítulo 18.
5.2 El flujo de eventos es el canal principal; los Hook son canales secundarios
- El
for awaitdeengine.run()es el canal principal: ordenado, con backpressure y cada evento aparece una sola vez; es adecuado para convertirlo directamente en tramas SSE / WebSocket. EngineHookes un callback secundario: síncrono, sin valor de retorno, las excepciones se ignoran; es adecuado para un segundo consumidor de los mismos datos (persistencia en disco, facturación, logs). No bases la UI únicamente en hooks, porque no garantizan backpressure.
6. Montaje del motor
Hay dos rutas:
- Ruta A:
createDefaultEngine(opts)— monta todas las capacidades integradas en una línea; adecuada para el 90 % de los casos; - Ruta B:
new TurnEngine(opts)— tú decides el provider, la tabla de herramientas, el motor de permisos y el prompt del sistema; adecuada para personalizaciones profundas o para reutilizar infraestructura existente.
6.1 Ruta A: createDefaultEngine
import { createDefaultEngine, Mode } from "@zqagent/core";
const engine = createDefaultEngine({
workspace: "/srv/workspaces/tenant-42",
model: "deepseek-chat",
mode: Mode.INTERACTIVE,
});Realiza estas acciones en orden (ver packages/core/src/index.ts:409):
- Si no se pasa
provider, creanew OpenAIProvider({ logDir }); - Resuelve la solución de sandbox con
resolveSandbox(); - Crea
ToolRegistryyConflictGuard, y registra 21 herramientas integradas (ver capítulo 7); - Crea
PermissionEngine(espacio de trabajo, modo, lista de comandos permitidos, política de shell, estado del sandbox); - Si no se pasa
instructions, genera el prompt del sistema conbuildInstructions(agentType, workspace, today, lang); - Ensambla
TurnEngine; sivcsestá activado, conectaVcsHooken paralelo medianteCompositeHook; - Inyecta un approver de respaldo según
defaultApproval.
Todos los parámetros de CreateEngineOpts
| Parámetro | Tipo / valor predeterminado | Descripción |
|---|---|---|
workspace | string (obligatorio) | Directorio raíz del espacio de trabajo = límite de permisos para archivos y comandos |
model | string; si se omite, lee OPENAI_MODEL | Identificador del modelo; si ambos están vacíos, run() genera un error explícito y no elige un modelo de respaldo |
provider | ProviderClient | Provider personalizado; si se omite, crea OpenAIProvider |
mode | Mode.INTERACTIVE | DISCUSS / PLAN / INTERACTIVE / AUTO |
maxIterations | DEFAULT_MAX_ITERATIONS = 800 | Número máximo de iteraciones por turno |
reasoningEffort | Si se omite, lee OPENAI_REASONING_EFFORT | low / medium / high; DeepSeek se omite automáticamente |
allowedCommands | [] | Prefijos de comandos permitidos automáticamente en modo Auto |
shellPolicy | "approve" | approve / sandbox / workspace / all |
sandbox | Si se omite, auto | { mode, extraWritable, ... }, ver 8.4 |
vcs | { mode: "off", sessionId: "default" } | Snapshot de Git por turno, ver capítulo 17 |
instructions | Generado por buildInstructions | Sobrescribe el prompt del sistema |
agentType | "code" | code (asistente de programación) / cowork (colaboración general); afecta al prompt predeterminado |
lang | Chino simplificado | zh / en / zh-TW / ja / ko / es; afecta al prompt, la denominación y el resumen |
messages | [] | Historial de conversación inicial (se pasa al restaurar una sesión) |
hook | Ninguno | Hooks de ciclo de vida, ver capítulo 11 |
subagentHookFactory | Ninguno | Genera un hook de subagente por cada llamada a explore |
contextWindow | Detección automática; si falla, 128K | Ventana de contexto del modelo (tokens) |
thresholdPct | 0.8 | Umbral de compactación dura (proporción de la ventana) |
softThresholdPct | 75 % del umbral duro | Umbral suave: solo elimina resultados antiguos de herramientas, sin llamar al LLM |
capTokens | null | Límite absoluto del umbral duro (tokens); null = sin límite |
toolResultDir | tool-results dentro del directorio de estado | Directorio de persistencia para resultados de herramientas que superan el presupuesto; en multiinquilino debe aislarse por inquilino |
logDir | No se registra | Directorio raíz de logs de auditoría de solicitudes LLM (subdirectorios por fecha) |
defaultApproval | "error" | Cuando no se inyecta approver: error / deny / allow |
lang solo afecta al prompt predeterminado y al idioma de la denominación de sesión y de los resúmenes; si se pasa instructions explícitamente, prevalece este.
6.2 Ruta B: montaje manual de 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); // 别忘了Todos los parámetros de TurnEngineOpts
| Parámetro | Obligatorio | Descripción |
|---|---|---|
provider | Sí | ProviderClient (stream() / chat()) |
registry | Sí | ToolRegistry |
permissions | Sí | PermissionEngine |
model | Sí | Identificador del modelo; una cadena vacía hará que run() lance un error |
instructions | Sí | Prompt del sistema; se inyecta como primer mensaje system |
genSessionNamePrompt | No | Prompt para generar el nombre de la sesión; si se omite, se toma según lang |
lang | No | Predeterminado "zh"; afecta a la denominación y al resumen de compactación |
maxIterations | No | Predeterminado 800 |
finalizeOnMaxIterations | No | Tras alcanzar el límite, ejecuta un turno final sin herramientas; predeterminado false; los subagentes usan true |
reasoningEffort | No | Se pasa al provider |
messages | No | Historial inicial; si el primero no es system, se inserta instructions automáticamente |
hook | No | EngineHook |
contextWindow | No | Sobrescribe la detección automática |
thresholdPct | No | Predeterminado 0.8 |
softThresholdPct | No | Predeterminado 75 % del umbral duro |
capTokens | No | Predeterminado null |
toolResultDir | No | Directorio de persistencia de resultados de herramientas |
6.3 Qué se puede cambiar en tiempo de ejecución
Propiedades y métodos públicos de 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. Herramientas: integradas y personalizadas
7.1 Las 21 herramientas que registra createDefaultEngine
| Categoría | Herramienta | Riesgo | Función de registro |
|---|---|---|---|
| Archivos | read_file, list_files | Solo lectura | registerFileTools / registerReadOnlyFileTools |
| Archivos | write_file, replace_in_file | Escritura local | registerFileTools |
| Shell | run_shell, shell_task_output, shell_task_kill | Ejecución | registerShellTool |
| Búsqueda | grep | Solo lectura | registerSearchTool |
| Red | web_search, web_fetch | Externo | registerWebTools |
| Git | git_log, git_status, git_diff | Solo lectura | registerGitTools |
| Parches | apply_patch, apply_unified_diff | Escritura local | registerDiffTools |
| Colaboración | ask_user, propose_plan, request_directory | Interacción | registerAskTool / registerPlanTool / registerDirectoryTool |
| Tareas | todo_write | Solo lectura/estado | registerTodoTool |
| Subagentes | explore | Solo lectura | registerExploreTool |
| Medios | media | Externo | registerMediaTool |
Las herramientas de terminal no están en el montaje de una línea:
terminal_list/terminal_readdependen deTerminalSourceproporcionado por el host (PTY persistente del panel «Terminal» de la consola), y deben inyectarse manualmente:tsimport { registerTerminalTools, type TerminalSource } from "@zqagent/core"; registerTerminalTools(registry, { terminals: myTerminalManager, // 实现 TerminalSource projectId: "tenant-42", });
projectIdes el límite de aislamiento y debe pasarse por proyecto; de lo contrario, el proyecto A podría leer el contenido de terminal del proyecto B.
Consulta Herramientas integradas para ver los parámetros y el comportamiento completos de cada herramienta.
7.2 Herramientas personalizadas
Firma de 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;Ejemplo:
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",
);Consideraciones:
- El nombre de la herramienta proviene de
schema.function.name; si no se pasa schema, se usa el nombre de la función y la estructura de parámetros queda vacía, por lo que el modelo normalmente no la invocará correctamente. Por ello, las herramientas formales deben proporcionar siempre un schema explícito. funcpuede devolver una Promise; el motor haráawait.- El segundo parámetro
ctx.signales la señal de interrupción. Las herramientas de larga duración deben pasarla a la tarea subyacente; de lo contrario, después de que el usuario interrumpa, la herramienta seguirá ejecutándose hasta terminar. - Un
registercon el mismo nombre reemplaza la herramienta anterior y puede usarse para sobrescribir implementaciones integradas;remove()la elimina;disable()/enable()controlan si aparece enschemas()(invisible para el modelo). - El resultado devuelto al modelo pasa por un presupuesto de resultados: si supera el umbral, se guarda en disco y se entrega al modelo un resumen/ruta; se puede ajustar con las variables de entorno
TOOL_RESULT_MAX_CHARS,TOOL_RESULT_SAVE_THRESHOLDyTOOL_RESULT_DIR. - Las excepciones lanzadas por la herramienta son capturadas por el motor, convertidas en
{ error, error_type }como resultado de la herramienta, y se deja que el modelo decida el siguiente paso; no hacen que todo el turno falle.
7.3 Conjunto de herramientas de solo lectura
Para subagentes o escenarios de solo lectura:
import { registerReadOnlyFileTools } from "@zqagent/core";
registerReadOnlyFileTools(registry, workspace); // read_file / list_files8. Permisos, aprobación y límites de seguridad
8.1 Flujo de decisión
PermissionEngine.evaluate() decide en un orden fijo (packages/core/src/permissions/PermissionEngine.ts:173):
- Modo de solo lectura (
DISCUSS/PLAN) → las operaciones con efectos secundarios se deniegan directamente; - Ruta de escritura de archivo fuera de
workspaceRoot→ se deniega directamente; - Riesgo de solo lectura (
RiskClass.READ) → se permite directamente; - Modo
AUTO→ todas las operaciones con efectos secundarios se permiten; - Política de exención de shell (
shellPolicy+ sandbox) → puede permitirse directamente; - Lista de comandos permitidos / lista de comandos permitidos de la sesión → se permite directamente;
- Lista de herramientas permitidas de la sesión (el usuario eligió «permitir siempre esta herramienta») → se permite directamente;
- El resto →
needsUser: true, se entrega al approver.
Estructura de retorno:
interface Decision {
allowed: boolean;
reason: string; // 展示给用户 / 审批卡片
needsUser: boolean; // true = 需要审批
}8.2 Modo de ejecución
enum Mode {
DISCUSS = "discuss", // 只读,禁止写入与执行
PLAN = "plan", // 只读,禁止写入与执行
INTERACTIVE = "interactive", // 有副作用操作逐次审批(推荐默认)
AUTO = "auto", // 有副作用操作全部放行
}interactive es el valor predeterminado y también el modo más seguro para usuarios reales. auto solo debería usarse en entornos aislados, espacios de trabajo desechables o entradas totalmente confiables.
8.3 Callback de aprobación
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", // 拒绝
}Formas de implementación:
- CLI / escritorio: bloquea a la espera de que el usuario pulse un botón en la interfaz;
- Servicio web: coloca la solicitud en una Promise, envía la tarjeta de aprobación al cliente y hace resolve al recibir el callback —esto es la «aprobación suspendida» de
@zqagent/server; - Worker sin supervisión: devuelve
ONCEautomáticamente según la lista de permitidos yDENYpara el resto (odefaultApproval: "allow", pero primero confirma que el espacio de trabajo está aislado).
Es obligatorio implementar un approver. El valor predeterminado defaultApproval: "error" lanzará un error la primera vez que se requiera autorización; esto es un diseño intencional: evita casos como «olvidé conectar la aprobación → la herramienta no se ejecuta silenciosamente → el usuario cree que el modelo está roto».
8.4 Exención de aprobación para shell y sandbox del SO
shellPolicy:
| Valor | Comportamiento |
|---|---|
approve (predeterminado) | Todo requiere aprobación salvo lo que esté en la lista de permitidos |
sandbox | Solo se exime de aprobación cuando el sandbox a nivel de SO está realmente activo; de lo contrario, requiere aprobación |
workspace | Sandbox activo → exento de aprobación; no activo → se usa la ruta del espacio de trabajo como criterio de respaldo |
all | Todo shell exento de aprobación (alto riesgo; equivale a un auto que solo aplica al shell) |
Configuración del sandbox:
createDefaultEngine({
workspace,
sandbox: {
mode: "auto", // auto / off / required
extraWritable: ["/data/cache"], // 额外可写路径
},
});- macOS:
sandbox-exec, restringefile-write*al espacio de trabajo + directorio temporal; - Linux:
bwrap, montaje de solo lectura para todo el sistema + espacio de trabajo escribible; - Windows: no hay un sandbox de sistema de archivos equivalente a nivel de proceso;
enforced: false, se degrada a heurísticas de rutas (esto es una estrategia para reducir molestias, no un límite de seguridad).
En modo required, si no se detecta el sandbox se genera un error explícito; adecuado para despliegues que exigen el aislamiento como requisito de cumplimiento.
8.5 Lista de comandos permitidos
createDefaultEngine({
workspace,
mode: Mode.AUTO,
allowedCommands: ["git status", "git diff", "ls", "cat", "pnpm test"],
});Reglas:
- Las reglas de coincidencia son por prefijo, pero deben terminar en un límite de palabra:
lsno permitelsof; - Los comandos que contienen operadores de shell (
; & | > < \$( ( \n) **nunca se permiten automáticamente**, para evitar quels; rm -rf /` los eluda; - Después de que el usuario elija «permitir siempre este comando» dentro de la sesión, se recuerda por nombre del ejecutable y solo aplica a comandos simples.
8.6 Lista de límites de seguridad
- No establezcas
workspaceen/, en el directorio home del usuario ni en un directorio que contenga secretos; - Usa
Mode.INTERACTIVEpor defecto; no pongasAUTOpor defecto solo por la «experiencia de automatización»; - En escenarios multiinquilino, cada inquilino debe tener su propio
workspace,toolResultDir, directorio deFileLogHooky configuración de MCP; - Las herramientas MCP tienen
requiresApproval: truepor defecto; no desactives la aprobación de forma global; logs/logDirregistran el contenido de las solicitudes LLM; decide si activarlo y cuánto tiempo conservarlo según la clasificación de datos;- El contenido de páginas web/archivos devuelto por herramientas es datos no confiables; no lo ejecutes como instrucciones (el prompt del motor ya incluye restricciones relevantes, pero el lado de negocio igualmente debe validar la salida).
9. Referencia completa del flujo de eventos
9.1 Estructura
interface WsEvent {
type: EventType;
data: Record<string, unknown>; // 字段随类型变化
}run() devuelve AsyncGenerator<WsEvent>. Debe consumirse por completo (o usar break y dejar que el generador sea recolectado); de lo contrario, el motor no continuará avanzando. Orden típico:
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_endRuta de excepción: después de error se emite además un turn_end; ruta de interrupción: después de interrupted se emite turn_end. El consumidor debe usar turn_end como estado final de la máquina de estados de un turno, no el fin del flujo.
9.2 Tipos de evento y campos
| Evento | Campos clave de data | Cuándo aparece |
|---|---|---|
turn_start | taskId, input, attachmentCount?, attachments? | Al inicio de cada turno |
assistant_delta | text, reqId, taskId | Incremento de texto en streaming del modelo |
reasoning_delta | text, reqId, taskId | Incremento del proceso de razonamiento (algunos modelos) |
assistant_message | text, reasoning, tool_calls (array de nombres de herramientas), usage?, reqId, taskId, logTime | Fin de una respuesta LLM |
tool_proposed | reqId (= ID de llamada a herramienta), name, arguments, taskId | El modelo propone invocar una herramienta |
permission_required | taskId, reqId, name, arguments, reason, category | Requiere aprobación (después se llama al approver) |
tool_started | taskId, reqId, name, startTime? | La herramienta empieza a ejecutarse |
tool_finished | taskId, reqId, name, status, reason?, duration?, result_preview? | La herramienta termina |
iteration_end | iteration, taskId | Fin de una iteración (LLM + herramientas) |
turn_end | status, iterations, produced_files, taskId | Fin de un turno (siempre se emite) |
error | error, error_type, retriable, iteration, reqId, logTime, taskId, stack? | Error del motor |
interrupted | iterations, reqId, taskId | Interrupción del usuario |
compacting | taskId, trigger, tokens, estimated, contextWindow, thresholdPct, thresholdTokens, outboundMessages | Inicio de la compactación |
compacted | reqId, taskId, method, messagesBefore, messagesAfter, boundaryIndex?, model?, summaryPreview?, keptUserMessages?, droppedUserMessages? | Compactación completada |
queue_steering | text, taskId, attachments? | Se inyecta la instrucción adicional en cola |
llm_request | reqId, taskId, logTime, model, msgCount, toolCount, compact? | Antes de cada solicitud LLM (para auditoría) |
ask_user_question | taskId, question, options, allowText, multi | El modelo invoca ask_user |
plan_proposed | plan | El modelo invoca propose_plan |
directory_requested | reason, path, writable | El modelo invoca request_directory |
llm_retry | attempt, max, reason, waitMs, iteration, reqId | Reintento automático por error de red transitorio |
session_named | name, source | Enumeración reservada; actualmente la CLI/server la emiten por su cuenta tras asignar el nombre; TurnEngine no la produce |
Valores de tool_finished.status:
| status | Significado |
|---|---|
ok | Ejecución correcta |
error | La herramienta lanza una excepción o devuelve un error |
denied | Permiso denegado o rechazo del usuario |
interrupted | Interrumpido durante la ejecución |
Valores de turn_end.status:
| status | Significado |
|---|---|
completed | El modelo ya no solicita herramientas; fin normal |
max_iterations_exceeded | Se alcanzó maxIterations |
interrupted | Interrupción del usuario |
error | Terminación por error del LLM/motor |
9.3 Ejemplo de consumo: convertir a 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();
});Nota: una desconexión SSE no aborta automáticamente el motor. Cuando el cliente se desconecte, debes llamar a engine.requestInterrupt(); de lo contrario, el motor seguirá ejecutándose hasta terminar y consumirá tokens.
9.4 Errores y reintentos
error.retriable === true: se puede intentarengine.retry()(solo es válido cuando el turno anterior terminó con error y al final hay un error reintentable).- Los 429/5xx los reintenta el propio SDK de OpenAI; los errores de red transitorios los reintenta automáticamente el motor
NETWORK_RETRY_MAX(predeterminado 2) veces, con base de backoffNETWORK_RETRY_BASE_MS(predeterminado 800 ms), y durante ese periodo emitellm_retry. - El desbordamiento de contexto se compacta automáticamente y se reintenta, sin reportar error al exterior; solo se reporta error y se marca como reintentable si la compactación también falla.
10. Proveedor de modelos
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", // 可选:请求审计日志
});Puntos clave de comportamiento:
- Se prioriza la API Responses; ante fallos, se degrada a Chat Completions según el tipo de error:
- El endpoint no lo soporta (404/405, etc.) → se degrada y se cachea por modelo;
- Parámetros incompatibles (algunas pasarelas compatibles) → degradación solo para esta vez;
- 401/429/5xx/error de red → no se degrada; se lanza tal cual.
- La variable de entorno
OPENAI_FORCE_API_MODE=responses|chatpuede forzar el modo. - Si el nombre del modelo o la baseURL contienen
deepseek, la ruta de Responses inyecta automáticamente la herramienta integradaweb_searchdel lado del servidor y filtra la herramienta local con el mismo nombre. logDircrea subdirectorios por fecha y escribe archivos de auditoría de solicitud/respuesta porreqId; incluye el contenido de los mensajes, así que decide si activarlo según los requisitos de cumplimiento de datos.
Variables de entorno relacionadas:
| Variable | Función |
|---|---|
OPENAI_API_KEY | Clave de API |
OPENAI_BASE_URL | Endpoint compatible |
OPENAI_MODEL | Modelo predeterminado |
OPENAI_REASONING_EFFORT | low / medium / high |
OPENAI_FORCE_API_MODE | responses / chat |
OPENAI_MAX_RETRIES | Número de reintentos del SDK |
OPENAI_TIMEOUT_MS | Tiempo de espera de solicitud |
OPENAI_STREAM_IDLE_MS | Watchdog de inactividad del stream |
SOCKS5_PROXY | LLM y herramientas de red usan proxy SOCKS5 |
10.2 Provider personalizado
El motor solo depende de dos métodos (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;
}Los dos puntos más importantes al implementar un provider personalizado:
- El último chunk debe incluir
toolCallsofinishReason; de lo contrario, el motor considera que el flujo no terminó correctamente; - Al recibir el abort de
opts.signal, debes dejar de leer de inmediato y lanzar/finalizar el flujo; de lo contrario, el botón «Detener» no tendrá efecto.
chat() se usa principalmente para solicitudes ligeras como la denominación de sesiones; incluso si devuelve null, no afecta al flujo principal.
10.3 Esfuerzo de razonamiento
createDefaultEngine({
workspace,
reasoningEffort: "high", // low / medium / high
});Se pasa al provider; tiene efecto en los modelos de la familia OpenAI y se omite automáticamente en DeepSeek (este usa un interruptor de thinking y no admite control por niveles).
11. Hooks de ciclo de vida y persistencia
11.1 Todos los callbacks de EngineHook
| Callback | Parámetros | Propósito |
|---|---|---|
onTurnStart | input | Inicio de un turno |
onTurnEnd | status, iterations, producedFiles | Fin de un turno |
onAssistantDelta | text | Texto en streaming |
onReasoningDelta | text | Razonamiento en streaming |
onAssistantMessage | text, toolCalls, usage? | Respuesta completa (habitual para facturación) |
onToolProposed | name, args | Propuesta de herramienta |
onToolStarted | name | Inicio de herramienta |
onToolFinished | name, status, resultPreview | Fin de herramienta |
onPermissionRequired | name, reason | Requiere aprobación |
onIterationEnd | iteration | Fin de iteración (habitual para checkpoints) |
onCompacting / onCompacted | info / text, info | Compactación |
onError | error, errorType, info? | Error (incluye retriable) |
onRetry | attempt, max, reason | Reintento automático por red |
onInterrupted | iterations | Interrupción |
onEvent | type, data | Respaldo para eventos no coincidentes |
Los hooks son callbacks síncronos y el motor no hace await. Por lo tanto:
- No realices operaciones bloqueantes prolongadas dentro de un hook (bloquearían el motor);
- Las excepciones lanzadas por un hook son ignoradas por el motor (try/catch interno de
_emitHook) y no afectan al flujo principal; - Para datos que requieren backpressure/garantía de orden, consume el flujo de eventos de
run()en lugar de usar hooks.
11.2 Combinar varios Hook
import { CompositeHook, ConsoleHook, FileLogHook } from "@zqagent/core";
const hook = new CompositeHook([
new ConsoleHook(),
new FileLogHook(sessionId, "/var/lib/myapp/sessions"),
]);createDefaultEngine({ vcs }) también usa CompositeHook internamente para conectar VcsHook en paralelo con el hook propio del invocador, sin sobrescribirlo.
11.3 FileLogHook: persistencia y restauración a nivel de archivos
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 });Campos de 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[];
}Al restaurar, ten en cuenta:
- Pasa
messagesanew TurnEngine({ messages })ocreateDefaultEngine({ messages }); - Restaura
permissionModey las listas de permitidos (engine.permissions.mode = ...,allowToolForSession/allowCommandForSession); de lo contrario, tras reiniciar se perderán las autorizaciones previas del usuario; FileLogHook.loadState()devuelvenullcuando no puede leer el archivo y no lanza error;- En escenarios multiinquilino, usa un
stateDirpor sesión; no mezcles directorios.
FileLogHook también ofrece los métodos onPaused / onResumed para registrar suspensión/restauración, pero no forman parte de la interfaz EngineHook; llámalos directamente sobre la instancia cuando sea necesario.
11.4 Uso del log de eventos
El log de eventos JSONL es la base para la auditoría (quién hizo que el Agent hiciera qué y cuándo) y la reproducción; no es la única fuente del estado de la sesión. Para cambios en el contenido de archivos, usa los snapshots de VCS del capítulo 17.
12. Control de un turno
12.1 Conversación multi-turno
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")); // 自动带上一轮上下文Las llamadas consecutivas sobre el mismo engine conservan el historial. Para iniciar una nueva sesión, crea un nuevo engine.
12.2 Interrupción
// 在另一个地方(例如 HTTP DELETE / 用户点「停止」)
engine.requestInterrupt();Hace lo siguiente:
- Activa la marca de cancelación;
- Aborta el flujo LLM en curso;
- Aborta la herramienta en ejecución (
run_shellmata el árbol de procesos hijos);
Después, el flujo de eventos producirá interrupted + turn_end(status: "interrupted"). No uses «dejar de consumir el flujo de eventos» en lugar de la interrupción: de ese modo el motor no avanzará ni limpiará el estado.
12.3 Reintento
for await (const e of engine.retry()) {
// 仅在上一轮以可重试错误结束时才会产出事件
}12.4 Restaurar desde una aprobación suspendida
engine.resume() se usa en escenarios donde «al final del historial hay llamadas a herramientas sin responder» (típicamente para restaurar aprobaciones suspendidas tras reiniciar el servicio):
for await (const e of engine.resume()) {
// 重新处理挂起的 tool_calls;approver 会按已 resolve 的结果立即返回
}Si no hay llamadas a herramientas suspendidas, resume() regresa directamente y no produce eventos.
12.5 Steering: añadir instrucciones en ejecución
engine.queueSteering("顺便把测试也跑一下");
engine.queueSteering("忽略 dist 目录", attachments, refs);El motor inyecta las instrucciones en cola como mensajes user en los intervalos entre iteraciones y produce queue_steering. Es adecuado para interacciones del tipo «el Agent está ejecutando una tarea larga y el usuario quiere añadir algo». Las instrucciones aún no inyectadas se pueden revocar:
engine.removeSteering("顺便把测试也跑一下"); // 已注入返回 false12.6 Restricciones de concurrencia
- Un mismo engine solo puede tener un turno en ejecución a la vez;
- A nivel de herramientas: las herramientas de bajo riesgo se ejecutan de forma concurrente; las de alto riesgo/que requieren aprobación/interactivas se ejecutan de forma serial;
- Tu capa de servicio debe poner en cola las llamadas a
run()de la misma sesión para evitar que dos solicitudes escriban enmessagesal mismo tiempo.
13. Adjuntos
13.1 Estructura de datos
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"
}Límites (packages/core/src/engine/attachments.ts):
| Constante | Valor | Significado |
|---|---|---|
MAX_ATTACHMENTS | 4 | Número máximo de adjuntos por vez |
MAX_ATTACHMENT_BYTES | 10 MB | Límite por archivo y también total |
ATTACHMENT_DIR | "attachments" | Subdirectorio dentro del directorio de sesión |
13.2 Flujo recomendado: guardar primero en disco y luego pasar referencias
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 circula con los mensajes y eventos, y puede usarse para renderizar miniaturas en la UI; los bytes en sí solo están en <sessionDir>/attachments/. Para leerlos, usa:
const { name, mimeType, size, base64 } = readStoredAttachment(sessionDir, ref.path);Consideraciones de seguridad:
readStoredAttachmentsolo acepta la formaattachments/<file>y rechaza..y subdirectorios;- Los adjuntos entran en las solicitudes LLM; el log de auditoría
logDirregistrará los mensajes (si se registra base64 depende de la implementación de logs del provider; en producción debe evaluarse); - No renderices directamente como adjuntos de texto HTML/scripts arbitrarios subidos por el usuario.
14. Compactación de contexto
Las conversaciones largas no pueden meterse indefinidamente en la ventana del modelo; el motor compacta automáticamente.
14.1 Mecanismo de activación
| Concepto | Predeterminado | Descripción |
|---|---|---|
| Ventana de contexto | Detección automática; si falla, 128K | Se puede sobrescribir con contextWindow |
| Umbral duro | 80 % de la ventana | thresholdPct |
| Umbral suave | 75 % del umbral duro (≈60 % de la ventana) | softThresholdPct, solo elimina resultados antiguos de herramientas |
| Límite absoluto | Ninguno | capTokens |
| Conservar lo reciente | Aproximadamente 25 % | Proporción de conversación reciente conservada tras la compactación |
| Límite de salida del resumen | 3000 tokens | Fijado internamente |
Flujo:
- Se alcanza el umbral suave: se eliminan resultados antiguos de herramientas y se deduplican
read_filerepetidos, sin llamar al LLM; - Se alcanza el umbral duro: se llama al LLM para generar un resumen y se emiten
compacting/compacted; - La API reporta desbordamiento de contexto: se fuerza la compactación y se reintenta.
14.2 Eventos y hooks
// engine.run(...) 的事件流中会出现:
// { type: "compacting", data: { trigger: "threshold", tokens, contextWindow, ... } }
// { type: "compacted", data: { method: "summarized" | "trimmed", messagesBefore, messagesAfter, ... } }Los hooks onCompacting / onCompacted reciben la misma información. La UI puede usarla para mostrar «compactando contexto».
14.3 Parámetros ajustables y variables de entorno
createDefaultEngine({
workspace,
contextWindow: 200_000,
thresholdPct: 0.85,
softThresholdPct: 0.7,
capTokens: 120_000,
});| Variable de entorno | Función |
|---|---|
COMPACTION_MODEL | Modelo usado para el resumen (si se omite, se usa el modelo actual) |
COMPACTION_REASONING_EFFORT | Esfuerzo de razonamiento del resumen (predeterminado low) |
COMPACTION_ELIDE | 0 desactiva la eliminación de resultados antiguos de herramientas |
COMPACTION_DEDUP | 0 desactiva la deduplicación de lecturas repetidas |
CONTEXT_WINDOW | Sobrescritura de la ventana al montar en el servidor (la lee @zqagent/server) |
COMPACTION_THRESHOLD_PCT / COMPACTION_SOFT_THRESHOLD_PCT / COMPACTION_CAP_TOKENS | Sobrescritura de umbrales al montar en el servidor |
CompactionStatey las funciones de utilidad de compactación actualmente no se exportan desde la raíz del paquete; los terceros solo necesitan observar el estado medianteengine.compactionStateo depender de eventos/hooks. No importes implementaciones internas mediante rutas profundas.
15. Integración con MCP
MCP (Model Context Protocol) permite que el Agent use servidores de herramientas externos. @zqagent/core incluye el cliente y la lógica de registro de herramientas.
15.1 Uso mínimo
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();Una vez establecida la conexión, registry.schemas() incluirá automáticamente las herramientas MCP y el modelo podrá verlas en la siguiente llamada LLM (sin necesidad de reconstruir el engine).
15.2 Formato de configuración
<workspace>/.agent/mcp.json admite varias formas:
// 裸数组
[
{ "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": ["..."] } } }- Si se omite
type, se infiere en el ordenbuiltin→url→command; commandpuede ser una cadena o un array;${VAR}/${env:VAR}se expanden con variables de entorno;enabled: falseodisabled: truelo desactivan;- Los presets integrados (OCR) se añaden automáticamente si el usuario no tiene una configuración con el mismo nombre;
{ "name": "ocr", "enabled": false }puede desactivarlos.
15.3 Nomenclatura de herramientas y aprobación
- Formato del nombre registrado:
mcp__<server>__<tool>(ambos segmentos pasan por limpieza y truncado de caracteres); - Metadatos:
category: "mcp",riskLevel: "high",requiresApproval: true; - Por lo tanto, es obligatorio conectar un approver; de lo contrario, todas las herramientas MCP usarán la política de respaldo predeterminada.
15.4 Carga perezosa (cuando hay muchas herramientas)
new McpManager(registry, configs, {
lazy: {
enabled: true, // 或交给阈值自动判断
thresholdTools: 16,
thresholdChars: 24_000,
maxLoaded: 40,
},
});Al activarse, solo se registran dos metaherramientas:
mcp_search_tools(solo lectura, sin aprobación): busca herramientas por palabra clave y carga automáticamente las primeras 8 coincidencias;mcp_call_tool(requiere aprobación): invoca por nombre una herramienta cargada/resoluble.
La variable de entorno ZQAGENT_MCP_LAZY=1/0 puede forzar la activación/desactivación.
15.5 Ciclo de vida
await manager.connectServer("github");
await manager.refresh("github"); // 断开 → 重连 → 重新注册
manager.attach(newRegistry); // 引擎重建时迁移已加载工具
await manager.closeAll(); // 进程退出时McpManager solo gestiona memoria y no persiste la configuración: después de addConfig / updateConfig / removeConfig, debes llamar tú mismo a saveMcpConfigFile().
15.6 OCR integrado
El preset ocr expone mcp__ocr__recognize, admite extracción de texto de imágenes/PDF y degrada en el orden macOS Vision → tesseract → LLM multimodal. builtin.llm debe recibir la configuración del provider de la sesión actual; de lo contrario, el tercer nivel de degradación no estará disponible.
16. Subagentes y registro de Agent
16.1 Subagente explore
createDefaultEngine ya registra la herramienta explore (packages/core/src/tools/explore.ts):
- El agente principal delega una «tarea de investigación» al subagente;
- El subagente usa un contexto independiente y una tabla de herramientas independiente, y se ejecuta en
Mode.PLAN(bloqueo estricto de escritura); - Por defecto solo incluye herramientas de archivos de solo lectura, búsqueda y herramientas Git de solo lectura;
- Por defecto, máximo 30 turnos (
EXPLORE_MAX_ITERATIONS); al alcanzar el límite, ejecuta un turno final y emite un informe; - El agente principal solo recibe el informe final; el proceso intermedio del subagente no entra en el contexto principal.
Personalización del comportamiento del subagente:
createDefaultEngine({
workspace,
subagentHookFactory: () => new SubagentHook("explore"), // 每个子代理一个实例
});Si no se pasa subagentHookFactory, el subagente es completamente silencioso. SubagentHook imprime las llamadas a herramientas del subagente con sangría en la terminal y es la implementación de referencia de la CLI; en escenarios Web puedes implementar tu propio hook para reenviar los eventos del subagente al frontend (nota: los eventos del subagente no pasan por el flujo de eventos del run() principal).
Registro manual:
registerExploreTool(registry, {
workspace,
provider,
model,
lang: "zh",
hookFactory: () => mySubagentHook,
toolResultDir,
});16.2 AgentRegistry
AgentRegistry es una tabla de roles en memoria que gestiona «nombre + descripción + prompt del sistema + modelo opcional»:
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 solo gestiona memoria; la persistencia (por ejemplo, agents.json) la implementa el invocador.
17. Seguimiento de cambios y reversión
El Agent modifica archivos. El log de eventos solo registra la «intención de la herramienta», no el contenido de los archivos; para responder «qué se cambió exactamente en este turno y cómo revertirlo», usa snapshots de VCS.
17.1 Activación
const engine = createDefaultEngine({
workspace,
vcs: { mode: "auto", sessionId: "tenant-42-session-7" },
});mode:
| Valor | Comportamiento |
|---|---|
off (predeterminado) | Desactivado |
auto | Si es un repositorio Git, usa modo nativo; de lo contrario, usa repositorio shadow |
native | Usa el .git del espacio de trabajo, pero con index independiente y refs/agent/*, sin tocar HEAD/staging del usuario |
shadow | Crea un repositorio shadow en <workspace>/.agent/vcs |
Tanto native como shadow requieren que git esté disponible en la máquina; si no se encuentra, se degrada a off (la detección de conflictos de escritura sigue funcionando).
17.2 Consulta y reversión
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 ya conecta onTurnStart → beginTurn() y onTurnEnd → commitTurn(status); createDefaultEngine({ vcs }) lo compone automáticamente. Si necesitas gestionar el ciclo de vida por tu cuenta, usa directamente new VcsHook(vcs) y añádelo a tu composición de hooks.
Notas:
- El
turndevuelto porcommitTurn()siempre es 0; el número de turno real proviene delistTurns(); - El
turndediffTurn/restoreTurnes el número de «turno con cambios»; los turnos vacíos no ocupan número; - El contenido del snapshot es el estado del espacio de trabajo tras
git add -A, incluidos archivos no rastreados (sujeto a reglas de exclusión como.agent/,.git/,node_modules/, etc.).
18. Cuatro patrones de integración y esqueletos de código
18.1 Patrón uno: servicio HTTP + SSE (el más parecido a @zqagent/server)
Diseño clave:
Map<sessionId, SessionRuntime>; cada runtime contiene el engine, el búfer de eventos y la Promise pendiente de aprobación;- Las llamadas a
run()de la misma sesión se serializan en cola; - La aprobación implementa la «suspensión» mediante el evento
permission_required+ una Promise pendiente de resolve; - Al desconectarse el cliente,
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 Patrón dos: aplicación de escritorio (Electron / sidecar de Tauri)
- El motor se ejecuta en el proceso principal o en el proceso sidecar, y la UI recibe eventos por IPC;
workspaceapunta al directorio de trabajo elegido por el usuario;- La aprobación usa diálogos nativos;
- Las herramientas de archivos y shell se ejecutan directamente en la máquina local; asegúrate de mantener
INTERACTIVEy el sandbox; - Antes de salir,
await mcpManager.closeAll()yhook.saveState().
18.3 Patrón tres: Worker sin supervisión
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;
});En escenarios de Worker, presta especial atención a:
- Un workspace independiente por job; al terminar, limpiar o archivar;
- Establece límites de
maxIterations,contextWindowycapTokenspara evitar que los tokens se descontrolen; - Usa
onAssistantMessagepara contabilizar el uso; - Usa
onTurnEndpara evaluarstatus; si no escompleted, considéralo fallo y regístralo.
18.4 Patrón cuatro: SaaS multiinquilino
Dimensiones que deben aislarse:
| Recurso | Forma de aislamiento |
|---|---|
| Sistema de archivos | workspace independiente por inquilino |
| Persistencia de resultados de herramientas | toolResultDir independiente por inquilino/sesión |
| Eventos y checkpoints | stateDir independiente por sesión (FileLogHook) |
| Snapshots de Git | sessionId por sesión → refs/agent/<id> |
| MCP | Origen de configuración por inquilino y McpBuiltinContext.workspace |
| Aprobación | Las solicitudes de aprobación deben llevar contexto de inquilino/usuario; no se puede compartir una cola global |
| Credenciales del modelo | Instancia de provider por inquilino; no pongas la key en el prompt/parámetros de herramientas |
No reutilices engine, registry (las herramientas MCP se contaminarían entre sí) ni permissions (las listas de permitidos se mezclarían) entre inquilinos.
19. Lista de verificación para producción
Confirma cada punto antes de publicar:
Montaje
- [ ]
modelconfigurado explícitamente; si no está configurado, falla al iniciar; - [ ]
workspacees un directorio independiente por inquilino/tarea, no/ni el directorio home; - [ ]
modepredeterminadoINTERACTIVE; - [ ]
engine.setApprover()implementado y con respaldo para timeout/desconexión; - [ ]
setQuestionHandler()/setPlanHandler()/setDirectoryHandler()implementados (si se usan las herramientas correspondientes); - [ ]
maxIterations,contextWindowycapTokenstienen límites explícitos; - [ ]
toolResultDiraislado por inquilino.
Seguridad
- [ ] La política de shell y el estado del sandbox cumplen lo esperado (
engine.permissions.sandbox); - [ ] La lista de comandos permitidos no contiene comodines/comandos compuestos;
- [ ] Las herramientas MCP mantienen la aprobación y el archivo de configuración no contiene claves en texto plano (usa
${ENV}); - [ ] Los límites de tamaño/cantidad de adjuntos se validan de nuevo en la capa externa;
- [ ] El periodo de retención y la política de desensibilización de los logs de auditoría están definidos.
Confiabilidad
- [ ] Las llamadas a
run()de la misma sesión están serializadas; - [ ] Cliente desconectado →
requestInterrupt(); - [ ] Salida del proceso →
mcpManager.closeAll(),hook.saveState(); - [ ] Restauración tras reinicio → leer
SavedState.messagesy restaurar el modo de permisos/listas de permitidos; - [ ] Proporcionar una entrada de «reintentar» para
error.retriable; - [ ] Monitorizar la distribución de
turn_end.statusy el uso de tokens.
Rendimiento
- [ ] Los eventos de una sesión no se almacenan en búfer indefinidamente (SSE se reenvía directamente, sin grandes arrays en memoria);
- [ ]
logDirse divide por fecha y se limpia periódicamente; - [ ] Los resultados grandes de herramientas pasan por el presupuesto de persistencia en disco y no se reinyectan completos al modelo.
20. Referencia rápida de API
20.1 Exportaciones del punto de entrada principal (extracto)
// 引擎
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 Variables de entorno habituales
| Variable | Función |
|---|---|
OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_MODEL | Configuración del Provider |
OPENAI_REASONING_EFFORT | Esfuerzo de razonamiento |
OPENAI_FORCE_API_MODE | responses / chat |
OPENAI_MAX_RETRIES / OPENAI_TIMEOUT_MS / OPENAI_STREAM_IDLE_MS | Fiabilidad de solicitudes |
SOCKS5_PROXY | Proxy SOCKS5 |
MAX_ITERATIONS | Límite de iteraciones al montar en el servidor |
CONTEXT_WINDOW / COMPACTION_THRESHOLD_PCT / COMPACTION_SOFT_THRESHOLD_PCT / COMPACTION_CAP_TOKENS | Parámetros de compactación al montar en el servidor |
COMPACTION_MODEL / COMPACTION_REASONING_EFFORT / COMPACTION_ELIDE / COMPACTION_DEDUP | Comportamiento de compactación |
NETWORK_RETRY_MAX / NETWORK_RETRY_BASE_MS | Reintento por errores de red transitorios |
TOOL_RESULT_MAX_CHARS / TOOL_RESULT_SAVE_THRESHOLD / TOOL_RESULT_DIR | Presupuesto de resultados de herramientas |
EXPLORE_MAX_ITERATIONS | Límite de iteraciones del subagente (predeterminado 30) |
MCP_CALL_TIMEOUT_MS | timeout de llamada a herramienta MCP (predeterminado 300 s) |
ZQAGENT_MCP_LAZY | Interruptor de carga perezosa de MCP |
21. FAQ de solución de problemas
P: La herramienta no se ejecutó y el modelo dice «el usuario la rechazó». El approver devolvió DENY, o se configuró defaultApproval: "deny". Revisa los logs del approver.
P: Error «la herramienta X requiere aprobación, pero no se inyectó un approver al montar el motor». No se llamó a engine.setApprover(). Este es el comportamiento esperado de defaultApproval: "error" predeterminado. Ten en cuenta que este error lo lanza el approver y emerge directamente del for await; el invocador necesita try/catch (o cambiar a deny / proporcionar un approver real).
P: Error «no se especificó un modelo». Pasa model o configura OPENAI_MODEL. El motor no elige un modelo por ti.
P: Error «OPENAI_API_KEY no está configurada». Pasa un provider personalizado o configura OPENAI_API_KEY.
P: run() nunca termina. Comprueba si se consumió por completo el flujo de eventos; comprueba si una herramienta está bloqueada (las tareas largas de run_shell deben ir en segundo plano); comprueba si el provider está ignorando opts.signal.
P: No ocurre nada después de pulsar «Detener». Debes llamar a engine.requestInterrupt(), no dejar de leer el flujo de eventos. Un provider personalizado debe responder a opts.signal.
P: Tras reiniciar se perdió el «permitir siempre» anterior del usuario.SavedState contiene permissionMode / allowTools / allowCommands; al restaurar debes escribirlos manualmente de vuelta en engine.permissions.
P: Varias sesiones en el mismo espacio de trabajo sobrescriben archivos entre sí. Cada sesión tiene un engine independiente, pero el sistema de archivos es compartido. ConflictGuard (integrado) puede detectar «modificado por otro después de la lectura»; para un aislamiento total, da a cada sesión un workspace independiente o activa la auditoría vcs.
P: Las herramientas MCP no aparecen.connectAll() es asíncrono y hay que esperar a que termine; la tabla de herramientas se lee en tiempo real en cada llamada LLM, por lo que aparecerán en el siguiente turno tras completar la conexión. Revisa la lista failed y la ruta de configuración.
P: En un proyecto CommonJS, require() produce ERR_PACKAGE_PATH_NOT_EXPORTED. Usa la versión 0.2.60 o superior; los artefactos CJS de versiones anteriores fallan al cargarse debido a dependencias solo ESM.
P: Las solicitudes al modelo deben pasar por un proxy. Configura SOCKS5_PROXY=socks5://host:port; los proxies HTTP/HTTPS aún no son compatibles.
22. Versión, build y publicación
Versión actual del paquete:
0.3.25(packages/core/package.json).Licencia: Apache-2.0.
Build:
bashpnpm --filter @zqagent/core build # tsup → packages/core/dist(ESM + CJS + d.ts) pnpm --filter @zqagent/core typecheck pnpm --filter @zqagent/core test # VitestPublicación:
bash publish_npm.sh [patch|minor|major](solo core), o el script de repositorioscripts/publish-all-npm.sh(shared → core → server → cli).Forma de los artefactos:
dist/index.js(ESM),dist/index.cjs(CJS),dist/index.d.ts,dist/index.d.cts;@zqagent/sharedse instala junto como dependencia externa.
Recomendaciones de actualización:
- Fija la versión exacta y ejecuta las pruebas de integración antes de actualizar;
- Vigila los cambios en
TurnEngineOpts/CreateEngineOpts/ campos de eventos (los campos nuevos suelen ser compatibles; las eliminaciones/renombrados se indican en las notas de la versión mayor); - No trates
internal/*como API estable; puede cambiar al actualizar.
Documentación relacionada
