Skip to content

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.


Índice ​

  1. Qué es y qué no es
  2. Posicionamiento arquitectónico y relación entre paquetes
  3. Instalación y entorno de ejecución
  4. Poner en marcha en cinco minutos
  5. Conceptos centrales
  6. Montaje del motor
  7. Herramientas: integradas y personalizadas
  8. Permisos, aprobación y límites de seguridad
  9. Referencia completa del flujo de eventos
  10. Proveedor de modelos
  11. Hooks de ciclo de vida y persistencia
  12. Control de un turno
  13. Adjuntos
  14. Compactación de contexto
  15. Integración con MCP
  16. Subagentes y registro de Agent
  17. Seguimiento de cambios y reversión
  18. Cuatro patrones de integración y esqueletos de código
  19. Lista de verificación para producción
  20. Referencia rápida de API
  21. FAQ de solución de problemas
  22. 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 PermissionEngine si 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 (ConsoleHook es solo una implementación de referencia);
  • servicios HTTP / WebSocket / SSE (@zqagent/server es una implementación de referencia);
  • usuarios, inquilinos, facturación y autenticación;
  • indexación de sesiones y base de datos (FileLogHook es 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:

PaquetePropósito¿Se puede usar de forma independiente?
@zqagent/coreMotor Agent (protagonista de este artículo)Sí
@zqagent/sharedMódulos base compartidos por core y hubSe instala automáticamente como dependencia de core
@zqagent/serverImplementación de referencia de servicio HTTP multiinquilinoSí, pero pertenece a la capa de aplicación
@zqagent/cliImplementación de referencia de aplicación de terminalSí, pero pertenece a la capa de aplicación

3. Instalación y entorno de ejecución ​

3.1 Requisitos del entorno ​

ElementoRequisito
Node.js>= 18.17 (engines de package.json)
Sistema de módulosDoble artefacto ESM y CommonJS; tanto import como require funcionan
TiposLas declaraciones de tipos se publican con el paquete (dist/index.d.ts / .d.cts)
Sistema operativoLinux / macOS / Windows; la herramienta shell selecciona automáticamente un shell disponible en Windows

3.2 Instalación ​

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

Verificación de versión: este artículo corresponde al repositorio 0.3.25. La versión disponible en npm se determina con npm view @zqagent/core version; si la versión de npm está por detrás del repositorio, se puede ejecutar pnpm --filter @zqagent/core build dentro del repositorio y luego referenciarlo mediante file: / workspace. Se recomienda fijar la versión exacta en package.json para 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:

jsonc
{
  "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/:

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

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+):

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

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

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

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

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

Configura antes de ejecutar:

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

Este ú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á.

ConceptoTipoResponsabilidad
EngineTurnEngineRuntime de una sesión. Contiene el historial de mensajes, el provider, la tabla de herramientas y el motor de permisos
TurnUna llamada a engine.run()Una entrada del usuario → varias iteraciones LLM/herramientas → un turn_end
IterationBucle internoUna llamada al LLM + toda la ejecución de herramientas solicitada por esa respuesta
EventWsEventEl único canal en tiempo real de salida del motor (generador asíncrono)
ToolToolSpecFunción invocable por el modelo + JSON Schema + metadatos de riesgo
ApproverApproverCuando 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.messages es el historial de la sesión y es intrínsecamente secuencial;
  • el «permitir siempre en esta sesión» dentro de engine.permissions es estado a nivel de sesión;
  • engine.compactionState es 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 await de engine.run() es el canal principal: ordenado, con backpressure y cada evento aparece una sola vez; es adecuado para convertirlo directamente en tramas SSE / WebSocket.
  • EngineHook es 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 ​

ts
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):

  1. Si no se pasa provider, crea new OpenAIProvider({ logDir });
  2. Resuelve la solución de sandbox con resolveSandbox();
  3. Crea ToolRegistry y ConflictGuard, y registra 21 herramientas integradas (ver capítulo 7);
  4. Crea PermissionEngine (espacio de trabajo, modo, lista de comandos permitidos, política de shell, estado del sandbox);
  5. Si no se pasa instructions, genera el prompt del sistema con buildInstructions(agentType, workspace, today, lang);
  6. Ensambla TurnEngine; si vcs está activado, conecta VcsHook en paralelo mediante CompositeHook;
  7. Inyecta un approver de respaldo según defaultApproval.

Todos los parámetros de CreateEngineOpts ​

ParámetroTipo / valor predeterminadoDescripción
workspacestring (obligatorio)Directorio raíz del espacio de trabajo = límite de permisos para archivos y comandos
modelstring; si se omite, lee OPENAI_MODELIdentificador del modelo; si ambos están vacíos, run() genera un error explícito y no elige un modelo de respaldo
providerProviderClientProvider personalizado; si se omite, crea OpenAIProvider
modeMode.INTERACTIVEDISCUSS / PLAN / INTERACTIVE / AUTO
maxIterationsDEFAULT_MAX_ITERATIONS = 800Número máximo de iteraciones por turno
reasoningEffortSi se omite, lee OPENAI_REASONING_EFFORTlow / medium / high; DeepSeek se omite automáticamente
allowedCommands[]Prefijos de comandos permitidos automáticamente en modo Auto
shellPolicy"approve"approve / sandbox / workspace / all
sandboxSi se omite, auto{ mode, extraWritable, ... }, ver 8.4
vcs{ mode: "off", sessionId: "default" }Snapshot de Git por turno, ver capítulo 17
instructionsGenerado por buildInstructionsSobrescribe el prompt del sistema
agentType"code"code (asistente de programación) / cowork (colaboración general); afecta al prompt predeterminado
langChino simplificadozh / 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)
hookNingunoHooks de ciclo de vida, ver capítulo 11
subagentHookFactoryNingunoGenera un hook de subagente por cada llamada a explore
contextWindowDetección automática; si falla, 128KVentana de contexto del modelo (tokens)
thresholdPct0.8Umbral de compactación dura (proporción de la ventana)
softThresholdPct75 % del umbral duroUmbral suave: solo elimina resultados antiguos de herramientas, sin llamar al LLM
capTokensnullLímite absoluto del umbral duro (tokens); null = sin límite
toolResultDirtool-results dentro del directorio de estadoDirectorio de persistencia para resultados de herramientas que superan el presupuesto; en multiinquilino debe aislarse por inquilino
logDirNo se registraDirectorio 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 ​

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

const workspace = process.cwd();

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

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

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

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

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

Todos los parámetros de TurnEngineOpts ​

ParámetroObligatorioDescripción
providerSíProviderClient (stream() / chat())
registrySíToolRegistry
permissionsSíPermissionEngine
modelSíIdentificador del modelo; una cadena vacía hará que run() lance un error
instructionsSíPrompt del sistema; se inyecta como primer mensaje system
genSessionNamePromptNoPrompt para generar el nombre de la sesión; si se omite, se toma según lang
langNoPredeterminado "zh"; afecta a la denominación y al resumen de compactación
maxIterationsNoPredeterminado 800
finalizeOnMaxIterationsNoTras alcanzar el límite, ejecuta un turno final sin herramientas; predeterminado false; los subagentes usan true
reasoningEffortNoSe pasa al provider
messagesNoHistorial inicial; si el primero no es system, se inserta instructions automáticamente
hookNoEngineHook
contextWindowNoSobrescribe la detección automática
thresholdPctNoPredeterminado 0.8
softThresholdPctNoPredeterminado 75 % del umbral duro
capTokensNoPredeterminado null
toolResultDirNoDirectorio 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:

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

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

7. Herramientas: integradas y personalizadas ​

7.1 Las 21 herramientas que registra createDefaultEngine ​

CategoríaHerramientaRiesgoFunción de registro
Archivosread_file, list_filesSolo lecturaregisterFileTools / registerReadOnlyFileTools
Archivoswrite_file, replace_in_fileEscritura localregisterFileTools
Shellrun_shell, shell_task_output, shell_task_killEjecuciónregisterShellTool
BúsquedagrepSolo lecturaregisterSearchTool
Redweb_search, web_fetchExternoregisterWebTools
Gitgit_log, git_status, git_diffSolo lecturaregisterGitTools
Parchesapply_patch, apply_unified_diffEscritura localregisterDiffTools
Colaboraciónask_user, propose_plan, request_directoryInteracciónregisterAskTool / registerPlanTool / registerDirectoryTool
Tareastodo_writeSolo lectura/estadoregisterTodoTool
SubagentesexploreSolo lecturaregisterExploreTool
MediosmediaExternoregisterMediaTool

Las herramientas de terminal no están en el montaje de una línea: terminal_list / terminal_read dependen de TerminalSource proporcionado por el host (PTY persistente del panel «Terminal» de la consola), y deben inyectarse manualmente:

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

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

projectId es 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):

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

Ejemplo:

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

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.
  • func puede devolver una Promise; el motor hará await.
  • El segundo parámetro ctx.signal es 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 register con el mismo nombre reemplaza la herramienta anterior y puede usarse para sobrescribir implementaciones integradas; remove() la elimina; disable() / enable() controlan si aparece en schemas() (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_THRESHOLD y TOOL_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:

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

8. 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):

  1. Modo de solo lectura (DISCUSS / PLAN) → las operaciones con efectos secundarios se deniegan directamente;
  2. Ruta de escritura de archivo fuera de workspaceRoot → se deniega directamente;
  3. Riesgo de solo lectura (RiskClass.READ) → se permite directamente;
  4. Modo AUTO → todas las operaciones con efectos secundarios se permiten;
  5. Política de exención de shell (shellPolicy + sandbox) → puede permitirse directamente;
  6. Lista de comandos permitidos / lista de comandos permitidos de la sesión → se permite directamente;
  7. Lista de herramientas permitidas de la sesión (el usuario eligió «permitir siempre esta herramienta») → se permite directamente;
  8. El resto → needsUser: true, se entrega al approver.

Estructura de retorno:

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

8.2 Modo de ejecución ​

ts
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 ​

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

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

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

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 ONCE automáticamente según la lista de permitidos y DENY para el resto (o defaultApproval: "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:

ValorComportamiento
approve (predeterminado)Todo requiere aprobación salvo lo que esté en la lista de permitidos
sandboxSolo se exime de aprobación cuando el sandbox a nivel de SO está realmente activo; de lo contrario, requiere aprobación
workspaceSandbox activo → exento de aprobación; no activo → se usa la ruta del espacio de trabajo como criterio de respaldo
allTodo shell exento de aprobación (alto riesgo; equivale a un auto que solo aplica al shell)

Configuración del sandbox:

ts
createDefaultEngine({
  workspace,
  sandbox: {
    mode: "auto",                  // auto / off / required
    extraWritable: ["/data/cache"], // 额外可写路径
  },
});
  • macOS: sandbox-exec, restringe file-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 ​

ts
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: ls no permite lsof;
  • Los comandos que contienen operadores de shell (; & | > < \ $( ( \n) **nunca se permiten automáticamente**, para evitar que ls; 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 workspace en /, en el directorio home del usuario ni en un directorio que contenga secretos;
  • Usa Mode.INTERACTIVE por defecto; no pongas AUTO por defecto solo por la «experiencia de automatización»;
  • En escenarios multiinquilino, cada inquilino debe tener su propio workspace, toolResultDir, directorio de FileLogHook y configuración de MCP;
  • Las herramientas MCP tienen requiresApproval: true por defecto; no desactives la aprobación de forma global;
  • logs / logDir registran 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 ​

ts
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_end

Ruta 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 ​

EventoCampos clave de dataCuándo aparece
turn_starttaskId, input, attachmentCount?, attachments?Al inicio de cada turno
assistant_deltatext, reqId, taskIdIncremento de texto en streaming del modelo
reasoning_deltatext, reqId, taskIdIncremento del proceso de razonamiento (algunos modelos)
assistant_messagetext, reasoning, tool_calls (array de nombres de herramientas), usage?, reqId, taskId, logTimeFin de una respuesta LLM
tool_proposedreqId (= ID de llamada a herramienta), name, arguments, taskIdEl modelo propone invocar una herramienta
permission_requiredtaskId, reqId, name, arguments, reason, categoryRequiere aprobación (después se llama al approver)
tool_startedtaskId, reqId, name, startTime?La herramienta empieza a ejecutarse
tool_finishedtaskId, reqId, name, status, reason?, duration?, result_preview?La herramienta termina
iteration_enditeration, taskIdFin de una iteración (LLM + herramientas)
turn_endstatus, iterations, produced_files, taskIdFin de un turno (siempre se emite)
errorerror, error_type, retriable, iteration, reqId, logTime, taskId, stack?Error del motor
interruptediterations, reqId, taskIdInterrupción del usuario
compactingtaskId, trigger, tokens, estimated, contextWindow, thresholdPct, thresholdTokens, outboundMessagesInicio de la compactación
compactedreqId, taskId, method, messagesBefore, messagesAfter, boundaryIndex?, model?, summaryPreview?, keptUserMessages?, droppedUserMessages?Compactación completada
queue_steeringtext, taskId, attachments?Se inyecta la instrucción adicional en cola
llm_requestreqId, taskId, logTime, model, msgCount, toolCount, compact?Antes de cada solicitud LLM (para auditoría)
ask_user_questiontaskId, question, options, allowText, multiEl modelo invoca ask_user
plan_proposedplanEl modelo invoca propose_plan
directory_requestedreason, path, writableEl modelo invoca request_directory
llm_retryattempt, max, reason, waitMs, iteration, reqIdReintento automático por error de red transitorio
session_namedname, sourceEnumeración reservada; actualmente la CLI/server la emiten por su cuenta tras asignar el nombre; TurnEngine no la produce

Valores de tool_finished.status:

statusSignificado
okEjecución correcta
errorLa herramienta lanza una excepción o devuelve un error
deniedPermiso denegado o rechazo del usuario
interruptedInterrumpido durante la ejecución

Valores de turn_end.status:

statusSignificado
completedEl modelo ya no solicita herramientas; fin normal
max_iterations_exceededSe alcanzó maxIterations
interruptedInterrupción del usuario
errorTerminación por error del LLM/motor

9.3 Ejemplo de consumo: convertir a SSE ​

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

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

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

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 intentar engine.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 backoff NETWORK_RETRY_BASE_MS (predeterminado 800 ms), y durante ese periodo emite llm_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 ​

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

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

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|chat puede forzar el modo.
  • Si el nombre del modelo o la baseURL contienen deepseek, la ruta de Responses inyecta automáticamente la herramienta integrada web_search del lado del servidor y filtra la herramienta local con el mismo nombre.
  • logDir crea subdirectorios por fecha y escribe archivos de auditoría de solicitud/respuesta por reqId; incluye el contenido de los mensajes, así que decide si activarlo según los requisitos de cumplimiento de datos.

Variables de entorno relacionadas:

VariableFunción
OPENAI_API_KEYClave de API
OPENAI_BASE_URLEndpoint compatible
OPENAI_MODELModelo predeterminado
OPENAI_REASONING_EFFORTlow / medium / high
OPENAI_FORCE_API_MODEresponses / chat
OPENAI_MAX_RETRIESNúmero de reintentos del SDK
OPENAI_TIMEOUT_MSTiempo de espera de solicitud
OPENAI_STREAM_IDLE_MSWatchdog de inactividad del stream
SOCKS5_PROXYLLM 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):

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

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

StreamChunk:

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

Los dos puntos más importantes al implementar un provider personalizado:

  1. El último chunk debe incluir toolCalls o finishReason; de lo contrario, el motor considera que el flujo no terminó correctamente;
  2. 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 ​

ts
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 ​

CallbackParámetrosPropósito
onTurnStartinputInicio de un turno
onTurnEndstatus, iterations, producedFilesFin de un turno
onAssistantDeltatextTexto en streaming
onReasoningDeltatextRazonamiento en streaming
onAssistantMessagetext, toolCalls, usage?Respuesta completa (habitual para facturación)
onToolProposedname, argsPropuesta de herramienta
onToolStartednameInicio de herramienta
onToolFinishedname, status, resultPreviewFin de herramienta
onPermissionRequiredname, reasonRequiere aprobación
onIterationEnditerationFin de iteración (habitual para checkpoints)
onCompacting / onCompactedinfo / text, infoCompactación
onErrorerror, errorType, info?Error (incluye retriable)
onRetryattempt, max, reasonReintento automático por red
onInterruptediterationsInterrupción
onEventtype, dataRespaldo 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 ​

ts
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 ​

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

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

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

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

Campos de SavedState:

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

Al restaurar, ten en cuenta:

  • Pasa messages a new TurnEngine({ messages }) o createDefaultEngine({ messages });
  • Restaura permissionMode y las listas de permitidos (engine.permissions.mode = ..., allowToolForSession / allowCommandForSession); de lo contrario, tras reiniciar se perderán las autorizaciones previas del usuario;
  • FileLogHook.loadState() devuelve null cuando no puede leer el archivo y no lanza error;
  • En escenarios multiinquilino, usa un stateDir por 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 ​

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

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

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

Las llamadas consecutivas sobre el mismo engine conservan el historial. Para iniciar una nueva sesión, crea un nuevo engine.

12.2 Interrupción ​

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

Hace lo siguiente:

  1. Activa la marca de cancelación;
  2. Aborta el flujo LLM en curso;
  3. Aborta la herramienta en ejecución (run_shell mata 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 ​

ts
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):

ts
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 ​

ts
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:

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

12.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 en messages al mismo tiempo.

13. Adjuntos ​

13.1 Estructura de datos ​

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

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

Límites (packages/core/src/engine/attachments.ts):

ConstanteValorSignificado
MAX_ATTACHMENTS4Número máximo de adjuntos por vez
MAX_ATTACHMENT_BYTES10 MBLí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 ​

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

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

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

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

StoredAttachment 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:

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

Consideraciones de seguridad:

  • readStoredAttachment solo acepta la forma attachments/<file> y rechaza .. y subdirectorios;
  • Los adjuntos entran en las solicitudes LLM; el log de auditoría logDir registrará 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 ​

ConceptoPredeterminadoDescripción
Ventana de contextoDetección automática; si falla, 128KSe puede sobrescribir con contextWindow
Umbral duro80 % de la ventanathresholdPct
Umbral suave75 % del umbral duro (≈60 % de la ventana)softThresholdPct, solo elimina resultados antiguos de herramientas
Límite absolutoNingunocapTokens
Conservar lo recienteAproximadamente 25 %Proporción de conversación reciente conservada tras la compactación
Límite de salida del resumen3000 tokensFijado internamente

Flujo:

  1. Se alcanza el umbral suave: se eliminan resultados antiguos de herramientas y se deduplican read_file repetidos, sin llamar al LLM;
  2. Se alcanza el umbral duro: se llama al LLM para generar un resumen y se emiten compacting / compacted;
  3. La API reporta desbordamiento de contexto: se fuerza la compactación y se reintenta.

14.2 Eventos y hooks ​

ts
// 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 ​

ts
createDefaultEngine({
  workspace,
  contextWindow: 200_000,
  thresholdPct: 0.85,
  softThresholdPct: 0.7,
  capTokens: 120_000,
});
Variable de entornoFunción
COMPACTION_MODELModelo usado para el resumen (si se omite, se usa el modelo actual)
COMPACTION_REASONING_EFFORTEsfuerzo de razonamiento del resumen (predeterminado low)
COMPACTION_ELIDE0 desactiva la eliminación de resultados antiguos de herramientas
COMPACTION_DEDUP0 desactiva la deduplicación de lecturas repetidas
CONTEXT_WINDOWSobrescritura de la ventana al montar en el servidor (la lee @zqagent/server)
COMPACTION_THRESHOLD_PCT / COMPACTION_SOFT_THRESHOLD_PCT / COMPACTION_CAP_TOKENSSobrescritura de umbrales al montar en el servidor

CompactionState y las funciones de utilidad de compactación actualmente no se exportan desde la raíz del paquete; los terceros solo necesitan observar el estado mediante engine.compactionState o 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 ​

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

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

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

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:

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

// 或 { "servers": [...] } / { "servers": { "name": {...} } }
// 或 Claude/Cursor 风格 { "mcpServers": { "name": { "command": "npx -y pkg", "args": ["..."] } } }
  • Si se omite type, se infiere en el orden builtin → url → command;
  • command puede ser una cadena o un array;
  • ${VAR} / ${env:VAR} se expanden con variables de entorno;
  • enabled: false o disabled: true lo 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) ​

ts
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 ​

ts
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:

ts
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:

ts
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»:

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

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

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

AgentRegistry 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 ​

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

mode:

ValorComportamiento
off (predeterminado)Desactivado
autoSi es un repositorio Git, usa modo nativo; de lo contrario, usa repositorio shadow
nativeUsa el .git del espacio de trabajo, pero con index independiente y refs/agent/*, sin tocar HEAD/staging del usuario
shadowCrea 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 ​

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

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

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

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

VcsHook 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 turn devuelto por commitTurn() siempre es 0; el número de turno real proviene de listTurns();
  • El turn de diffTurn / restoreTurn es 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().
ts
import {
  ApprovalOutcome,
  Mode,
  createDefaultEngine,
  type PermissionRequest,
} from "@zqagent/core";

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

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

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

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

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

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

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

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

18.2 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;
  • workspace apunta 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 INTERACTIVE y el sandbox;
  • Antes de salir, await mcpManager.closeAll() y hook.saveState().

18.3 Patrón tres: Worker sin supervisión ​

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

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

En escenarios de Worker, presta especial atención a:

  • Un workspace independiente por job; al terminar, limpiar o archivar;
  • Establece límites de maxIterations, contextWindow y capTokens para evitar que los tokens se descontrolen;
  • Usa onAssistantMessage para contabilizar el uso;
  • Usa onTurnEnd para evaluar status; si no es completed, considéralo fallo y regístralo.

18.4 Patrón cuatro: SaaS multiinquilino ​

Dimensiones que deben aislarse:

RecursoForma de aislamiento
Sistema de archivosworkspace independiente por inquilino
Persistencia de resultados de herramientastoolResultDir independiente por inquilino/sesión
Eventos y checkpointsstateDir independiente por sesión (FileLogHook)
Snapshots de GitsessionId por sesión → refs/agent/<id>
MCPOrigen de configuración por inquilino y McpBuiltinContext.workspace
AprobaciónLas solicitudes de aprobación deben llevar contexto de inquilino/usuario; no se puede compartir una cola global
Credenciales del modeloInstancia 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

  • [ ] model configurado explícitamente; si no está configurado, falla al iniciar;
  • [ ] workspace es un directorio independiente por inquilino/tarea, no / ni el directorio home;
  • [ ] mode predeterminado INTERACTIVE;
  • [ ] engine.setApprover() implementado y con respaldo para timeout/desconexión;
  • [ ] setQuestionHandler() / setPlanHandler() / setDirectoryHandler() implementados (si se usan las herramientas correspondientes);
  • [ ] maxIterations, contextWindow y capTokens tienen límites explícitos;
  • [ ] toolResultDir aislado 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.messages y restaurar el modo de permisos/listas de permitidos;
  • [ ] Proporcionar una entrada de «reintentar» para error.retriable;
  • [ ] Monitorizar la distribución de turn_end.status y 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);
  • [ ] logDir se 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) ​

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

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

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

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

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

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

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

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

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

20.2 Variables de entorno habituales ​

VariableFunción
OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_MODELConfiguración del Provider
OPENAI_REASONING_EFFORTEsfuerzo de razonamiento
OPENAI_FORCE_API_MODEresponses / chat
OPENAI_MAX_RETRIES / OPENAI_TIMEOUT_MS / OPENAI_STREAM_IDLE_MSFiabilidad de solicitudes
SOCKS5_PROXYProxy SOCKS5
MAX_ITERATIONSLímite de iteraciones al montar en el servidor
CONTEXT_WINDOW / COMPACTION_THRESHOLD_PCT / COMPACTION_SOFT_THRESHOLD_PCT / COMPACTION_CAP_TOKENSParámetros de compactación al montar en el servidor
COMPACTION_MODEL / COMPACTION_REASONING_EFFORT / COMPACTION_ELIDE / COMPACTION_DEDUPComportamiento de compactación
NETWORK_RETRY_MAX / NETWORK_RETRY_BASE_MSReintento por errores de red transitorios
TOOL_RESULT_MAX_CHARS / TOOL_RESULT_SAVE_THRESHOLD / TOOL_RESULT_DIRPresupuesto de resultados de herramientas
EXPLORE_MAX_ITERATIONSLímite de iteraciones del subagente (predeterminado 30)
MCP_CALL_TIMEOUT_MStimeout de llamada a herramienta MCP (predeterminado 300 s)
ZQAGENT_MCP_LAZYInterruptor 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:

    bash
    pnpm --filter @zqagent/core build     # tsup → packages/core/dist(ESM + CJS + d.ts)
    pnpm --filter @zqagent/core typecheck
    pnpm --filter @zqagent/core test      # Vitest
  • Publicación: bash publish_npm.sh [patch|minor|major] (solo core), o el script de repositorio scripts/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/shared se 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

Last updated: