ppxans-harness 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +265 -0
  3. package/bin/ppx-channels.js +3 -0
  4. package/bin/ppx-serve.js +6 -0
  5. package/bin/ppx.js +3 -0
  6. package/config/identity.md +6 -0
  7. package/config/ishiki.md +16 -0
  8. package/config/ppx.json +151 -0
  9. package/package.json +69 -0
  10. package/src/agent/context.js +179 -0
  11. package/src/agent/index.js +717 -0
  12. package/src/agent/prompts.js +107 -0
  13. package/src/aml-server.js +151 -0
  14. package/src/ans/eviction.js +144 -0
  15. package/src/ans/guard.js +120 -0
  16. package/src/ans/lifecycle.js +93 -0
  17. package/src/ans/proactive.js +129 -0
  18. package/src/ans/reward.js +112 -0
  19. package/src/ans/values.js +15 -0
  20. package/src/audit/audit-chain.js +167 -0
  21. package/src/audit/verifier.js +120 -0
  22. package/src/bus/circuit-breaker.js +115 -0
  23. package/src/bus/runtime-bus.js +94 -0
  24. package/src/channels/base.js +35 -0
  25. package/src/channels/feishu.js +127 -0
  26. package/src/channels/http.js +592 -0
  27. package/src/channels/index.js +110 -0
  28. package/src/channels/log.js +29 -0
  29. package/src/channels/wechat-crypto.js +74 -0
  30. package/src/channels/wechat.js +197 -0
  31. package/src/channels-cli.js +124 -0
  32. package/src/cli.js +120 -0
  33. package/src/config/channels.js +170 -0
  34. package/src/config/index.js +224 -0
  35. package/src/config/providers.js +189 -0
  36. package/src/config/settings.js +182 -0
  37. package/src/core/policy.js +272 -0
  38. package/src/core/trace.js +89 -0
  39. package/src/evolve/playbook.js +194 -0
  40. package/src/llm/client.js +446 -0
  41. package/src/llm/dsml.js +74 -0
  42. package/src/llm/embedder.js +35 -0
  43. package/src/llm/fence.js +105 -0
  44. package/src/llm/index.js +4 -0
  45. package/src/llm/retry.js +73 -0
  46. package/src/llm/router.js +98 -0
  47. package/src/mcp/client.js +375 -0
  48. package/src/mcp/index.js +116 -0
  49. package/src/memory/asset-hub.js +131 -0
  50. package/src/memory/canvas.js +131 -0
  51. package/src/memory/compaction.js +28 -0
  52. package/src/memory/experience.js +122 -0
  53. package/src/memory/fact-store.js +699 -0
  54. package/src/memory/failure-episode.js +99 -0
  55. package/src/memory/fork.js +83 -0
  56. package/src/memory/index.js +7 -0
  57. package/src/memory/l0.js +52 -0
  58. package/src/memory/l2.js +131 -0
  59. package/src/memory/l3.js +112 -0
  60. package/src/memory/memory-ticker.js +240 -0
  61. package/src/memory/session.js +398 -0
  62. package/src/mode/blackboard.js +49 -0
  63. package/src/mode/graph.js +41 -0
  64. package/src/mode/index.js +64 -0
  65. package/src/mode/legion.js +51 -0
  66. package/src/mode/plan-exec.js +50 -0
  67. package/src/mode/router.js +40 -0
  68. package/src/orchestrator/agent-worker.js +70 -0
  69. package/src/orchestrator/dag.js +83 -0
  70. package/src/orchestrator/index.js +2 -0
  71. package/src/orchestrator/legion.js +188 -0
  72. package/src/orchestrator/supervisor.js +177 -0
  73. package/src/persona/index.js +29 -0
  74. package/src/plugin/builtin.js +212 -0
  75. package/src/plugin/context.js +79 -0
  76. package/src/plugin/index.js +62 -0
  77. package/src/seam/registry.js +98 -0
  78. package/src/seam/shell.js +55 -0
  79. package/src/selfheal/evolve.js +68 -0
  80. package/src/selfheal/healer.js +167 -0
  81. package/src/selfheal/run.js +9 -0
  82. package/src/server.js +60 -0
  83. package/src/services/learning-service.js +177 -0
  84. package/src/services/memory-health.js +99 -0
  85. package/src/services/memory-service.js +160 -0
  86. package/src/skills/loader.js +150 -0
  87. package/src/skills/verify.js +100 -0
  88. package/src/tools/advanced.js +353 -0
  89. package/src/tools/builtin.js +298 -0
  90. package/src/tools/catalog.js +159 -0
  91. package/src/tools/command-guard.js +112 -0
  92. package/src/tools/custom.js +47 -0
  93. package/src/tools/delegate.js +297 -0
  94. package/src/tools/document.js +253 -0
  95. package/src/tools/governance.js +260 -0
  96. package/src/tools/index.js +11 -0
  97. package/src/tools/methods.js +178 -0
  98. package/src/tools/ocr.js +59 -0
  99. package/src/tools/seam.js +125 -0
  100. package/src/tools/selfmod.js +176 -0
  101. package/src/utils/logger.js +17 -0
  102. package/src/utils/pii.js +42 -0
  103. package/src/utils/store.js +108 -0
  104. package/src/utils/text.js +16 -0
  105. package/src/utils/trace.js +153 -0
  106. package/src/utils/winutf8.js +15 -0
@@ -0,0 +1,717 @@
1
+ // src/agent/index.js - Agent 引擎 (皮皮虾核心) v0.2 含工具调用
2
+ import { ensureUTF8Console } from "../utils/winutf8.js";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ import { TOOL_ERROR_PREFIX } from "../tools/index.js";
8
+ // 重构第一刀 (2026-09-14): 工具循环执行策略抽至 src/core/policy.js
9
+ // (探索熔断/重复检测/溢出降档/错误重试/结果裁剪/循环驱动), 重新导出保持测试与外部兼容
10
+ import { runToolLoop, LLM_FAILED_HINT } from "../core/policy.js";
11
+ // 重构第三刀 (2026-09-14): 结构化事件流 traceId 贯穿 (AsyncLocalStorage), 关键路径埋点
12
+ import { EventTracer, runWithTrace } from "../core/trace.js";
13
+ // 重构第二刀 (2026-09-14): 记忆升降级 + 自我学习收敛为独立服务, agent 只保留薄委托
14
+ import { MemoryService } from "../services/memory-service.js";
15
+ import { LearningService } from "../services/learning-service.js";
16
+ export { isOverflowError as _isOverflowError, trimToolResult, toToolContent } from "../core/policy.js";
17
+ import { logicalDay } from "../utils/store.js";
18
+ import { loadConfig } from "../config/index.js";
19
+ import { info, warn, error } from "../utils/logger.js";
20
+ import { Context, compose, loadPlugins } from "../plugin/index.js";
21
+ import { builtinPlugins, resolveLLM, resolveAllLLMs } from "../plugin/builtin.js";
22
+ import { registerMcpTools } from "../mcp/index.js";
23
+ import { Auditor } from "../audit/verifier.js";
24
+ // ANS 独立模块 (可更换): 价值对齐 / 自主任务生成 / 生命周期
25
+ import { Lifecycle } from "../ans/lifecycle.js";
26
+ import { suggestProactive, markTaskDone } from "../ans/proactive.js";
27
+ import { record as rewardRecord, status as rewardStatus } from "../ans/reward.js";
28
+ import { scan as evictionScan, status as evictionStatus } from "../ans/eviction.js";
29
+ import { installGuard, guardStatus, installGuardOnCatalog } from "../ans/guard.js";
30
+ import { SkillLoader } from "../skills/loader.js";
31
+ import { EvolutionEngine } from "../selfheal/evolve.js";
32
+ // 重构 (2026-09-15): 历史/上下文管理 + 提示词构建从 PPXAgent 类抽出为 mixin
33
+ // (context.js: 历史裁剪/token 预算/会话压缩; prompts.js: 技能清单/核心价值/DSML/画像/多模态)
34
+ import { contextMethods } from "./context.js";
35
+ import { promptMethods } from "./prompts.js";
36
+
37
+ const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
38
+ // 会话历史条数/token 预算已迁到 config.memory (max_history_items / history_token_budget)
39
+ // (上下文窗口常量 DEFAULT_CONTEXT_WINDOW / DEFAULT_CONTEXT_RATIO 与辅助 LLM 超时 AUX_LLM_TIMEOUT_MS
40
+ // 已随历史管理迁至 src/agent/context.js)
41
+ // (工具循环阈值 DEFAULT_MAX_TOOL_ROUNDS / EXPLORE_TOOLS 等已迁至 src/core/policy.js)
42
+
43
+ // (工具结果裁剪 trimToolResult / 溢出判定 isOverflowError / LLM 失败提示 LLM_FAILED_HINT
44
+ // 已迁至 src/core/policy.js, 本文件经 re-export 保持兼容)
45
+ // (多模态视觉 content 注入 visionUserContent 已迁至 src/agent/prompts.js)
46
+
47
+ export class PPXAgent {
48
+ constructor({ root = ROOT, configFile = null, plugins = [], dataDir = null, globalDataDir = null } = {}) {
49
+ this.root = root;
50
+ // dataDir 可覆盖: 显式参数 > PPX_DATA_DIR 环境变量 > 默认目录
51
+ // 默认目录: 包装在 node_modules 里时外置到 ~/.ppx (防卸载丢数据), 否则 root/data
52
+ this.dataDir = dataDir || process.env.PPX_DATA_DIR || this._defaultDataDir(root);
53
+ // 全局共享数据目录 (跨 agent 共享经验等): 显式参数 > PPX_AGENT_GLOBAL_DATA_DIR > 本地 dataDir
54
+ this.globalDataDir = globalDataDir || process.env.PPX_AGENT_GLOBAL_DATA_DIR || this.dataDir;
55
+ this.config = this._loadConfig(configFile);
56
+ this.userName = this.config.user?.name || "兄弟";
57
+
58
+ // 插件装配: ctx 预置基础服务, 按顺序装配内置 + 用户插件 (一切皆插件)
59
+ // P2⑧: 顶层 ctx 为 full-access 基座 (内置插件可信), 用户插件默认 restricted
60
+ this.ctx = new Context(null, { access: "full-access" });
61
+ this.ctx.provide("root", root);
62
+ this.ctx.provide("dataDir", this.dataDir);
63
+ this.ctx.provide("globalDataDir", this.globalDataDir);
64
+ this.ctx.provide("config", this.config);
65
+ this.ctx.provide("userName", this.userName);
66
+ this.ctx.provide("agent", this);
67
+ // 装配顺序: 内置插件 → 用户插件目录(声明式) → 构造函数传入插件(编程式)
68
+ const pluginsDir = path.join(root, this.config.plugins?.dir || "plugins");
69
+ compose(this.ctx, [...builtinPlugins, ...loadPlugins(pluginsDir), ...plugins]);
70
+
71
+ // 从 ctx 取服务, 设置公开属性 (向后兼容, 外部代码不变)
72
+ this.healer = this.ctx.consume("healer");
73
+ this.health = this.ctx.consume("health");
74
+ this.persona = this.ctx.consume("persona");
75
+ this.facts = this.ctx.consume("facts");
76
+ this.experience = this.ctx.consume("experience");
77
+ this.sessionStore = this.ctx.consume("sessions");
78
+ this.memory = this.ctx.consume("memory");
79
+ this.llm = this.ctx.consume("llm");
80
+ this.allProviders = this.ctx.consume("allProviders");
81
+ this.l0 = this.ctx.consume("l0");
82
+ this.scenes = this.ctx.consume("scenes");
83
+ this.personaStore = this.ctx.consume("personaStore");
84
+ this.traces = this.ctx.consume("traces");
85
+ // 重构第三刀: 结构化事件流 (记忆升降级/工具失败/spawn/自愈触发), 独立于工具轨迹
86
+ this.tracer = new EventTracer(this.dataDir);
87
+ this.bus = this.ctx.consume("bus");
88
+ // ⑧ 免疫系: 全局闸门挂到总线命令通道 (拦截+审计)
89
+ this.__guard = installGuard(this, { allowList: this.config.agent?.guardAllowList || [] });
90
+ // ⑦ Reward 闭环: 订阅总线工具成败, 自动更新行为倾向 (EWMA)
91
+ this.bus?.on("tool/result", (ev) => {
92
+ const { name, ok } = ev.payload || {};
93
+ if (name) { try { rewardRecord(this, { tool: name, ok: !!ok }); } catch {} }
94
+ });
95
+ this.tools = this.ctx.consume("tools");
96
+ // P0 (2026-09-15): 免疫闸门接入工具执行收口 (修 MERGE-REPORT 遗留 P2 —— guard 之前只盖总线命令,
97
+ // 工具走 catalog 绕过全局闸门)。共享同一 state: approveGuard 一次授权同时作用于总线+工具。
98
+ try {
99
+ this.__guardOnCatalog = installGuardOnCatalog(this.tools, this.__guard);
100
+ } catch (e) {
101
+ warn(`[guard] 工具收口接入失败: ${e.message}`);
102
+ }
103
+ this.scheduler = this.ctx.consume("scheduler");
104
+ // ⑤排泄自治: 每日扫描长期记忆做冗余识别/冷热分层 (幂等注册, 不重复)
105
+ try {
106
+ const hasE = (this.scheduler?.list?.() || []).some((j) => j.name === "eviction-daily");
107
+ if (!hasE) this.scheduler?.add({ name: "eviction-daily", cron: "02:00", type: "daily", action: () => { try { evictionScan(this); } catch {} } });
108
+ } catch {}
109
+ // 首次启动跑一次排遗扫描 (预热治理状态)
110
+ try { evictionScan(this); } catch {}
111
+ this.toolsEnabled = this.ctx.consume("toolsEnabled");
112
+ this._warnMissingCloudApi(); // 发布首启引导: 未配云端 key 时明确提示
113
+
114
+ // 主动通知 + 中断状态
115
+ this._notifyCb = null;
116
+ this._onToolEvent = null; // 工具事件回调
117
+ this._interrupted = false;
118
+ this._lastTurnUsedTools = false;
119
+ this._mcp = null; // MCP 连接句柄 (connectMcp 后赋值)
120
+ this._proactiveTimer = null; // 主动任务生成定时器
121
+ // 生命周期 (ANS 独立模块): born → growing → mature → evolving / reproducing
122
+ // v1.0.7 持久化: 状态落盘 data/memory/lifecycle.json, 跨进程/重启不归零 (P1)
123
+ this.lifecycle = new Lifecycle({ file: path.join(this.dataDir, "memory", "lifecycle.json") });
124
+ this.evolve = new EvolutionEngine(this, this.config.agent?.evolve || {});
125
+ // Auditor (P0①): 唯一“已验证写回”通道 + 已验证账本 (data/audit/verified.json)
126
+ this.auditor = new Auditor({ ledgerPath: path.join(this.dataDir, "audit", "verified.json") });
127
+ // 方法技能目录 (Superpowers 吸收): 供 _context 注入技能清单, LLM 按需 load_skill
128
+ try { this.skills = new SkillLoader(path.join(root, "skills")); } catch { this.skills = null; }
129
+
130
+ // 重构第二刀: 记忆协调服务 + 自我学习服务 (依赖注入, llm 用闭包实时取当前 provider)
131
+ this.memorySvc = new MemoryService({
132
+ getLlm: () => this.llm,
133
+ facts: this.facts,
134
+ scenes: this.scenes,
135
+ personaStore: this.personaStore,
136
+ experience: this.experience,
137
+ lifecycle: this.lifecycle,
138
+ tracer: this.tracer,
139
+ });
140
+ this.learningSvc = new LearningService({
141
+ getLlm: () => this.llm,
142
+ traces: this.traces,
143
+ skills: this.skills,
144
+ experience: this.experience,
145
+ lifecycle: this.lifecycle,
146
+ auditor: this.auditor,
147
+ tracer: this.tracer,
148
+ toolNames: () => this._toolNames(),
149
+ runTool: (name, args) => this.tools.call(name, args, { agent: this }),
150
+ });
151
+ // 注入 LLM 摘要器/提炼器 (依赖 service, 装配后注入)
152
+ this.memory.summarizer = (raw) => this.memorySvc.summarizeMemory(raw);
153
+ this.memory.setExtractor((u, a, related) => this.memorySvc.extractMemory(u, a, related));
154
+
155
+ // 应用 tools.disabled: 从 config/ppx.json 读取需禁用的工具, 启动时禁用 (设置 UI 写盘生效)
156
+ this._applyDisabledTools();
157
+
158
+ // 可选: 启动时自动连接 MCP 服务器 (config.mcp.auto_connect = true 时非阻塞连接)
159
+ if (this.config.mcp?.auto_connect && this.config.mcp.servers?.length) {
160
+ this.connectMcp()
161
+ .then((n) => { if (n) info(`[mcp] 自动连接 ${n} 个 MCP 工具`); })
162
+ .catch((e) => warn(`[mcp] 自动连接失败: ${e.message}`));
163
+ }
164
+
165
+ // 首次启动生成 L3 画像 (零依赖高频词统计, 同步快, 不调 LLM)
166
+ this._maybeRefreshPersona();
167
+ }
168
+
169
+
170
+
171
+ // 默认数据目录: 包装在 node_modules 里(全局/本地安装)时外置到 ~/.ppx, 否则 root/data
172
+ _defaultDataDir(root) {
173
+ if (String(root).includes("node_modules")) return path.join(os.homedir(), ".ppx");
174
+ return path.join(root, "data");
175
+ }
176
+
177
+ // 主动通知 + 中断 API
178
+ setNotify(cb) { this._notifyCb = typeof cb === "function" ? cb : null; }
179
+ // 工具调用过程可视化 - 回调 (tool名, 参数, 耗时, 状态) 供 Web UI 推送
180
+ setToolEvent(cb) { this._onToolEvent = typeof cb === "function" ? cb : null; }
181
+ // turn/step 分层: 推理轮次事件 (每轮工具循环发一次 step), 供军团 worker 上报进度
182
+ setStepEvent(cb) { this._onStepEvent = typeof cb === "function" ? cb : null; }
183
+ notify(message) { if (this._notifyCb) { try { this._notifyCb(String(message)); } catch (e) {} } }
184
+ interrupt() { this._interrupted = true; }
185
+ clearInterrupt() { this._interrupted = false; }
186
+
187
+ // 只读模式 (SDD 审查者, 吸收 Superpowers): 禁用一切修改/执行类工具, 只能读/查
188
+ // 供 spawn_agent review 循环的审查者角色 (worker 经 PPX_AGENT_READONLY=1 触发)
189
+ enableReadonlyMode() {
190
+ const disabled = [
191
+ "run_command", "write_file", "code_act", "create_skill",
192
+ "memory_add", "add_schedule",
193
+ "scene_create", "scene_describe", "spawn_agent", "refine_skill",
194
+ "refine", // v1.0.8: refine 会写经验库, 只读审查者也不应触发
195
+ ];
196
+ for (const t of disabled) { try { this.tools.disable(t); } catch {} }
197
+ this.readonly = true;
198
+ return this;
199
+ }
200
+
201
+ _loadConfig(configFile) {
202
+ return loadConfig(this.root, configFile);
203
+ }
204
+
205
+ // P1#9: LLM 结构化记忆提炼 (实现已迁 src/services/memory-service.js, 此处分发保持公共 API)
206
+ async _extractMemory(user, assistant, existing = []) {
207
+ return this.memorySvc.extractMemory(user, assistant, existing);
208
+ }
209
+
210
+ // 辅助 LLM 调用前置健康探测 (v1.0.7): 模型不可用 (本地服务未运行/远端不可达) 时快速跳过,
211
+ // 不发起 10s 超时等待 — 本地未运行的 health() 是 ECONNREFUSED 毫秒级失败, 开销可忽略
212
+ // (记忆/学习方法已用 service 内部版本, 此处保留供 _maybeCompact 等使用)
213
+ async _auxLlmReady() {
214
+ if (!this.llm) return false;
215
+ if (typeof this.llm.health !== "function") return true;
216
+ try { return await this.llm.health(); } catch { return false; }
217
+ }
218
+
219
+ // 用 LLM 把旧对话浓缩成语义摘要 (实现已迁 memory-service, 此处分发)
220
+ async _summarizeMemory(raw) {
221
+ return this.memorySvc.summarizeMemory(raw);
222
+ }
223
+
224
+ // P1: LLM 查询扩展 (实现已迁 memory-service, 此处分发)
225
+ async _expandQuery(q) {
226
+ return this.memorySvc.expandQuery(q);
227
+ }
228
+
229
+ // P1: 语义记忆检索 (实现已迁 memory-service, 此处分发)
230
+ async _memoryQuery(q, { limit = 5, scope = null } = {}) {
231
+ return this.memorySvc.query(q, { limit, scope });
232
+ }
233
+
234
+ // 重置某会话历史 (新会话): 删除事件日志
235
+ resetSession(sessionKey) { this.sessionStore.delete(sessionKey || "default"); }
236
+
237
+ // 对话主入口 (含工具调用循环)
238
+ async chat(userMsg, { persist = true, sessionKey = "default", mode = null } = {}) {
239
+ // 重构第三刀: 入口生成 traceId, 记忆/工具/学习子调用自动继承 (AsyncLocalStorage)
240
+ return runWithTrace(async () => {
241
+ this.clearInterrupt(); // 新一轮对话开始, 复位上一轮的中断状态
242
+ this.bus?.emit("chat/user", { userMsg, sessionKey }, { source: "agent.chat" });
243
+ let reply;
244
+ // 内核自主决策: 高置信简单指令本地处理, 不调 LLM
245
+ const local = (this.config.agent?.localIntent !== false) ? await this._localIntent(userMsg) : null;
246
+ if (local) {
247
+ reply = local;
248
+ } else {
249
+ // 模式分发: 编排策略可插拔 (react/single, 未来 plan-exec/multi-agent/graph 等)
250
+ const modeName = mode || this.config.agent?.mode || "react";
251
+ try {
252
+ reply = await this.ctx.consume("modes").run(modeName, this, userMsg, { sessionKey });
253
+ } catch (e) {
254
+ error("LLM 调用失败:", e.message);
255
+ reply = LLM_FAILED_HINT(e.message);
256
+ }
257
+ }
258
+
259
+ const usedTools = this._lastTurnUsedTools;
260
+ this._lastTurnUsedTools = false;
261
+ if (this._notifyCb && usedTools) this.notify("[done] 任务完成 (工具执行)。");
262
+
263
+ if (persist) {
264
+ this._pushTurn(sessionKey, String(userMsg), reply);
265
+ await this.memory.recordTurn(userMsg, reply);
266
+ this.bus?.emit("memory/record", { userMsg, reply }, { source: "agent.chat" });
267
+ // 记忆升降级协调器 (memory-service): L2 场景归档 + 用户主动经验学习 + L3 画像跨天刷新
268
+ this.memorySvc.afterTurn(userMsg, reply);
269
+ }
270
+ // 生命周期推进: 每次对话计数, 阶段转换 born→growing→mature
271
+ this.bus?.emit("chat/reply", { reply }, { source: "agent.chat" });
272
+ this._lifecycleTick();
273
+ this.evolve && this.evolve.tick();
274
+ return reply;
275
+ }, { sessionKey, channel: "chat", userMsg: String(userMsg).slice(0, 200) });
276
+ }
277
+
278
+ // 生命周期: 每次对话计数 + 阶段转换 (委托 ans/lifecycle 模块)
279
+ _lifecycleTick() {
280
+ this.lifecycle.tick();
281
+ }
282
+
283
+ // 生命周期摘要 (可观测, 委托 ans/lifecycle 模块)
284
+ lifecycleStatus() {
285
+ return this.lifecycle.status();
286
+ }
287
+
288
+ // Reward 行为倾向可观测 (⑦内分泌)
289
+ rewardStatus() {
290
+ return rewardStatus(this);
291
+ }
292
+
293
+ // 排泄治理可观测 (⑤遗忘-归档)
294
+ evictionStatus() {
295
+ return evictionStatus(this);
296
+ }
297
+
298
+ // 立即手动触发一次记忆治理扫描 (冗余识别 + 冷热分层)
299
+ runMemoryEviction() {
300
+ return evictionScan(this);
301
+ }
302
+
303
+ // 免疫闸门可观测 (⑧安全治理)
304
+ guardStatus() {
305
+ return guardStatus(this);
306
+ }
307
+
308
+ // 单次审批: 放行一个危险命令 verb (一次用完自动失效)
309
+ approveGuard(verb) {
310
+ return this.__guard ? this.__guard.approveOnce(String(verb)) : null;
311
+ }
312
+
313
+
314
+ // 流式对话: 返回 { text, history } 或回调 onDelta 推送增量
315
+ // 支持工具循环: 若消息触发工具调用, 走 _llmWithTools (触发 onTool 事件推送工具活动),
316
+ // 最终结果作为一次 delta 推送; 否则走 streamChat 逐字流式 [P1#7]
317
+ async chatStream(userMsg, { sessionKey = "default", onDelta, onTool, onStep } = {}) {
318
+ // 重构第三刀: 入口生成 traceId (无 LLM 降级 chat 时嵌套新 trace, 独立可追踪)
319
+ return runWithTrace(async () => {
320
+ if (!this.llm) return this.chat(userMsg, { sessionKey });
321
+ this.clearInterrupt(); // 新一轮对话开始, 复位中断状态
322
+ // 内核自主决策: 高置信简单指令本地处理
323
+ const local = (this.config.agent?.localIntent !== false) ? await this._localIntent(userMsg) : null;
324
+ if (local) { onDelta && onDelta(local); return local; }
325
+ const system = this._context(userMsg);
326
+ const history = await this._loadHistory(sessionKey);
327
+ const messages = [{ role: "system", content: system }, ...history, { role: "user", content: this._userContent(userMsg) }];
328
+
329
+ // 多模态路由: 消息含图片时优先 vision provider (否则图片发到文本后端无意义)
330
+ const hasImage = messages.some((m) => Array.isArray(m.content) && m.content.some((c) => c && c.type === "image_url"));
331
+ const activeLLM = hasImage ? (this._visionLLM() || this.llm) : this.llm;
332
+
333
+ // 挂工具事件透传 (供 onTool 推送)
334
+ const prevCb = this._onToolEvent;
335
+ if (onTool) this._onToolEvent = (ev) => { try { onTool(ev); } catch {} };
336
+ // 挂 step 事件透传 (供 onStep 推送推理轮次)
337
+ const prevStepCb = this._onStepEvent;
338
+ if (onStep) this._onStepEvent = (ev) => { try { onStep(ev); } catch {} };
339
+
340
+ let reply;
341
+ try {
342
+ // 无工具开启: 直连后端可逐字流式 (恢复打字机效果); 有工具时走工具循环保轨迹完整 [复审 P2]
343
+ if (!this.toolsEnabled && activeLLM.supportsStream) {
344
+ reply = await activeLLM.streamChat(messages, {
345
+ onDelta: (d) => { onDelta && onDelta(d); },
346
+ });
347
+ } else {
348
+ reply = await this._llmWithTools(messages, activeLLM);
349
+ if (onDelta) onDelta(reply);
350
+ }
351
+ } catch (e) {
352
+ warn("chatStream 失败, 降级非流式 chat:", e.message);
353
+ reply = await this.chat(userMsg, { sessionKey });
354
+ if (onDelta) onDelta(reply);
355
+ } finally {
356
+ this._onToolEvent = prevCb;
357
+ this._onStepEvent = prevStepCb;
358
+ }
359
+ this._pushTurn(sessionKey, String(userMsg), reply);
360
+ await this.memory.recordTurn(userMsg, reply);
361
+ return reply;
362
+ }, { sessionKey, channel: "chatStream" });
363
+ }
364
+
365
+ // 多 provider 回退: 依次尝试, 失败切下一个
366
+ // 多 provider 并发健康探测 + 回退: 只对可用 provider 调用, 避免串行等待 180s 超时
367
+ async _llmWithFallback(seedMessages) {
368
+ let clients = (this.allProviders || []).length ? this.allProviders : [this.llm];
369
+ // 多模态路由: 消息含图片 (image_url 块) 时, 优先 vision provider, 避免图片发到文本后端浪费
370
+ const hasImage = seedMessages.some((m) => Array.isArray(m.content) && m.content.some((c) => c && c.type === "image_url"));
371
+ if (hasImage) {
372
+ const visionClients = clients.filter((c) => c.vision);
373
+ if (visionClients.length) clients = visionClients;
374
+ else info("消息含图片但无 vision provider, 图片将无法被模型理解 (请配置 vision: true 的 provider)");
375
+ }
376
+ // 工具类任务: 优先原生 tool_calls 后端 (http)。
377
+ // openclaw 是完整 agent 运行时, 会拒绝围栏协议(视为伪协议); 实测 http 原生 tool_calls 全链路通过。
378
+ if (this.toolsEnabled) {
379
+ const native = clients.filter((c) => c.supportsNativeToolCalls);
380
+ const fence = clients.filter((c) => !c.supportsNativeToolCalls);
381
+ if (native.length) clients = [...native, ...fence];
382
+ }
383
+ if (clients.length > 1) {
384
+ try {
385
+ const states = await Promise.all(clients.map((c) => c.health ? c.health() : Promise.resolve(true)));
386
+ const healthy = clients.filter((_, i) => states[i]);
387
+ if (healthy.length) clients = healthy;
388
+ else info("所有 provider 健康探测失败, 按原配置顺序尝试兜底");
389
+ } catch (e) {
390
+ warn("health 探测异常, 按原顺序回退:", e.message);
391
+ }
392
+ }
393
+ let lastErr = null;
394
+ for (const client of clients) {
395
+ try {
396
+ return await this._llmWithTools(seedMessages, client);
397
+ } catch (e) {
398
+ lastErr = e;
399
+ warn("provider 失败, 切换下一个:", client.model, e.message);
400
+ }
401
+ }
402
+ throw lastErr || new Error("所有 provider 均失败");
403
+ }
404
+
405
+ // 统一工具执行入口 (供 http 原生 tool_calls + openclaw/dsh 围栏代理共用) [P0#1]
406
+ // v1.0.7: 移除未使用的 llmInstance 死参数, 所有工具执行统一走此入口 (trace/事件只此一份)
407
+ async _runTool(name, args) {
408
+ const t0 = Date.now();
409
+ this._lastTurnUsedTools = true;
410
+ this.bus?.emit("tool/call", { name, args }, { source: "agent._runTool" });
411
+ if (this._onToolEvent) { try { this._onToolEvent({ type: "start", tool: name, args, ts: Date.now() }); } catch {} }
412
+ const result = await this.tools.call(name, args, { agent: this, timeoutMs: Number(this.config.agent?.tool_timeout_ms) || 0 });
413
+ const ok = !result.startsWith(TOOL_ERROR_PREFIX);
414
+ if (name === "spawn_agent") this.tracer.event("agent/spawn", { args });
415
+ this.bus?.emit("tool/result", {
416
+ name,
417
+ ok,
418
+ args,
419
+ durationMs: Date.now() - t0,
420
+ error: ok ? null : result.slice(0, 300),
421
+ }, { source: "agent._runTool" });
422
+ this.traces.record({
423
+ tool: name,
424
+ args,
425
+ result: result.slice(0, 800),
426
+ ok,
427
+ durationMs: Date.now() - t0,
428
+ error: ok ? null : result,
429
+ });
430
+ if (this._onToolEvent) { try { this._onToolEvent({ type: "done", tool: name, args, ok, durationMs: Date.now() - t0, result: result.slice(0, 300), ts: Date.now() }); } catch {} }
431
+ return result;
432
+ }
433
+
434
+ // LLM + 工具调用循环 (带 provider 回退)
435
+ // 重构第一刀 (2026-09-14): 循环驱动/探索熔断/重复检测/溢出降档/错误重试
436
+ // 全部收敛到 src/core/policy.js runToolLoop, 此处只注入依赖, 策略可独立测试/替换。
437
+ async _llmWithTools(seedMessages, llmInstance = this.llm) {
438
+ return runToolLoop({
439
+ seedMessages,
440
+ llm: llmInstance,
441
+ tools: this.toolsEnabled ? this.tools.toOpenAI() : [],
442
+ config: this.config,
443
+ isInterrupted: () => this._interrupted,
444
+ onStep: (ev) => { if (this._onStepEvent) { try { this._onStepEvent(ev); } catch {} } },
445
+ runTool: (name, args) => this._runTool(name, args),
446
+ shrinkMessages: (messages, budget) => this._shrinkMessagesForOverflow(messages, budget),
447
+ histTokenCap: () => this._histTokenCap(),
448
+ onEvent: (type, payload) => this.tracer.event(type, payload),
449
+ // v1.6.0 第四刀: 超时重试决策 (幂等才重试) + 超时预算查询 (事件采集)
450
+ isIdempotentTool: (name) => this._toolIdempotent(name),
451
+ toolTimeoutOf: (name) => this._toolTimeoutOf(name),
452
+ });
453
+ }
454
+
455
+ // 工具是否幂等 (可安全超时重试): 读取工具元数据, 未声明默认 true (只读/查询类)
456
+ _toolIdempotent(name) {
457
+ try {
458
+ const m = this.tools && typeof this.tools.metaOf === "function" ? this.tools.metaOf(name) : null;
459
+ return m ? !!m.idempotent : true;
460
+ } catch { return true; }
461
+ }
462
+
463
+ // 工具超时预算 (ms, 0/无声明 = 用全局默认; 事件采集用)
464
+ _toolTimeoutOf(name) {
465
+ try {
466
+ const m = this.tools && typeof this.tools.metaOf === "function" ? this.tools.metaOf(name) : null;
467
+ return m ? (m.timeoutMs > 0 ? m.timeoutMs : (Number(this.config.agent?.tool_timeout_ms) || 0)) : (Number(this.config.agent?.tool_timeout_ms) || 0);
468
+ } catch { return Number(this.config.agent?.tool_timeout_ms) || 0; }
469
+ }
470
+
471
+ // 离线工具路由: 无 LLM 时识别简单工具指令
472
+ // ---- 内核自主决策: 本地意图预判层 (P2-7) ----
473
+ // 高置信简单指令(问候/时间/记忆/明确工具)本地处理, 不调 LLM, 省成本更快
474
+ async _localIntent(userMsg) {
475
+ const m = String(userMsg).trim();
476
+ // 纯问候/告别/感谢 (短句, 高置信)
477
+ const greet = /^(你好|您好|嗨|哈喽|hello|hi|在吗|早上好|晚上好|下午好|再见|拜拜|谢谢|感谢|辛苦了|good\s*(mom|afternoon|evening)|thanks?|bye)\s*[!。??]*$/i;
478
+ if (greet.test(m)) {
479
+ if (/再见|拜拜|bye/i.test(m)) return "再见兄弟, 有事随时喊我。";
480
+ if (/谢谢|感谢|辛苦/i.test(m)) return "客气啥, 应该的。";
481
+ return "在的兄弟, 说。";
482
+ }
483
+ // 时间/日期
484
+ if (/^(现在)?(几点|时间|日期|几号|今天|星期几)[!?。?]*$/i.test(m)) {
485
+ return `[工具] ${await this.tools.call("get_time", {})}`;
486
+ }
487
+ // 记忆查询: 你记得XXX / 上次聊过XXX (P1: LLM 查询扩展 + RRF 融合补语义召回)
488
+ if (/^(你)?(记得|还记得|上次聊过|关于)[::]?\s*(.+)/i.test(m)) {
489
+ const q = m.replace(/^(你)?(记得|还记得|上次聊过|关于)[::]?\s*/i, "").trim();
490
+ const res = await this._memoryQuery(q || m, { limit: 3 });
491
+ return res.length ? "我记得:\n" + res.map(r => `- ${r.content}`).join("\n") : `(记忆里没找到关于"${q || m}"的)`;
492
+ }
493
+ // 记住 XX
494
+ const add = m.match(/^记住[::]\s*(.+)$/i);
495
+ if (add) return `[工具] ${await this.tools.call("memory_add", { content: add[1].trim() })}`;
496
+ // 读文件 / 列目录 (明确工具指令)
497
+ const read = m.match(/^读文件\s+(.+)$/i);
498
+ if (read) return `[工具] ${await this.tools.call("read_file", { path: read[1].trim() })}`;
499
+ const list = m.match(/^列出?\s+(\S+)?$/i);
500
+ if (list) return `[工具] ${await this.tools.call("list_dir", { path: list[1] || "." })}`;
501
+ return null;
502
+ }
503
+
504
+ // ---- Session Replay: 从 l0 原始日志恢复某会话历史 (跨天/崩溃续跑) ----
505
+ replaySession(sessionKey = "default", { days = 7, limit = 40 } = {}) {
506
+ const msgs = [];
507
+ const now = new Date();
508
+ for (let d = days - 1; d >= 0; d--) {
509
+ const day = logicalDay(new Date(now.getTime() - d * 86400000));
510
+ const recs = this.l0.read(day, 2000);
511
+ for (const r of recs) {
512
+ if (r.sessionKey !== sessionKey) continue;
513
+ if (r.role === "user" || r.role === "assistant") msgs.push({ role: r.role, content: r.content, ts: r.timestamp });
514
+ }
515
+ }
516
+ msgs.sort((a, b) => (a.ts || 0) - (b.ts || 0));
517
+ return msgs.slice(-limit).map(({ role, content }) => ({ role, content }));
518
+ }
519
+
520
+ // 离线工具路由已并入 _localIntent (超集), 不再单独保留
521
+
522
+ // 工具名清单 (给 verifyLesson 做幻觉接地)
523
+ _toolNames() {
524
+ try {
525
+ if (this.tools && typeof this.tools.listDetailed === "function") return this.tools.listDetailed().map((t) => t.name);
526
+ if (this.tools && typeof this.tools.names === "function") return this.tools.names();
527
+ } catch {}
528
+ return [];
529
+ }
530
+
531
+ // 用户主动经验学习 (实现已迁 memory-service, 此处分发)
532
+ _learnFromTurn(userMsg, reply) {
533
+ return this.memorySvc.learnFromTurn(userMsg, reply);
534
+ }
535
+
536
+ // P3: 自我进化闭环 - 失败→经验 (实现已迁 learning-service, 此处分发保持公共 API)
537
+ async refine({ limit = 20 } = {}) {
538
+ return this.learningSvc.refine({ limit });
539
+ }
540
+
541
+ // P2: 自我进化闭环 (下) - 成功→技能 (实现已迁 learning-service, 此处分发保持公共 API)
542
+ async refineSkill({ limit = 50, minFreq = 2 } = {}) {
543
+ return this.learningSvc.refineSkill({ limit, minFreq });
544
+ }
545
+
546
+ // 用中自进化 (实现已迁 learning-service, 此处分发保持公共 API)
547
+ async upgradeSkill(id, { minUses = 3, limit = 40 } = {}) {
548
+ return this.learningSvc.upgradeSkill(id, { minUses, limit });
549
+ }
550
+
551
+ // L2 场景归档 (实现已迁 memory-service, 此处分发)
552
+ _archiveScenes() {
553
+ return this.memorySvc.archiveScenes();
554
+ }
555
+
556
+ // 可观测: 聚合各层状态 (记忆 L0-L3 / 轨迹 / 工具 / 经验 / 自愈)
557
+ // 顶层展平 traces.stats() 字段 (count/failed/failRate/slowTools), 向后兼容 web 前端
558
+ stats() {
559
+ const sessions = this.sessionStore ? this.sessionStore.list() : [];
560
+ const eventsTotal = sessions.reduce((a, s) => a + (s.count || 0), 0);
561
+ const tools = this.tools ? this.tools.listDetailed() : [];
562
+ return {
563
+ ...(this.traces && typeof this.traces.stats === "function" ? this.traces.stats() : {}),
564
+ agent: {
565
+ name: this.config.agent?.name || "ppx",
566
+ mode: this.config.agent?.mode || "react",
567
+ llm: this.llm ? (this.llm.backend || this.llm.model || "configured") : "none",
568
+ },
569
+ memory: {
570
+ l0: { events_total: eventsTotal, sessions: sessions.length },
571
+ l1: this.facts && this.facts.stats ? this.facts.stats() : {},
572
+ l2: this.scenes && this.scenes.count ? { scenes: this.scenes.count() } : {},
573
+ l3: this.personaStore && this.personaStore.stats ? this.personaStore.stats() : {},
574
+ ...(this.memory && this.memory.stats ? this.memory.stats() : {}),
575
+ },
576
+ tools: {
577
+ total: tools.length,
578
+ enabled: tools.filter((t) => t.enabled).length,
579
+ list: tools.map((t) => ({ name: t.name, enabled: t.enabled, category: t.category })),
580
+ },
581
+ skills: this.skills && typeof this.skills.list === "function"
582
+ ? this.skills.list().map((s) => ({ id: s.id, description: s.description }))
583
+ : [],
584
+ mcp: this._mcp ? { connected: true, count: this._mcp.count || 0 } : { connected: false, count: 0 },
585
+ experience: this.experience ? { lessons: this.experience.lessons.length } : {},
586
+ health: this.health || null,
587
+ };
588
+ }
589
+
590
+ // 主动任务生成 (ANS 自主性): 扫描 L1 记忆里的待办/偏好, 生成主动提醒 (委托 ans/proactive 模块)
591
+ // 返回可直接投递的文本 (保持兼容); 结构化数据见 startProactiveTicker 回调的 payload
592
+ async proactiveSuggest() {
593
+ const out = await suggestProactive(this);
594
+ return out ? out.text : null;
595
+ }
596
+
597
+ // 标记待办完成 (ANS): 之后不再主动提醒 (窗口去重 + 完成跟踪)
598
+ proactiveMarkDone(id) {
599
+ return markTaskDone(this, id);
600
+ }
601
+
602
+ // 启动主动任务生成定时器 (config.agent.proactive.enabled 才启用)
603
+ // 回调契约: cb(payload) → { ts, items: [{content, importance, source, id}], text }
604
+ startProactiveTicker(cb) {
605
+ const cfg = this.config.agent?.proactive;
606
+ if (!cfg || !cfg.enabled) return null;
607
+ const ms = Number(cfg.interval_ms) || 3600000;
608
+ this._proactiveTimer = setInterval(async () => {
609
+ try {
610
+ const payload = await suggestProactive(this);
611
+ if (payload && typeof cb === "function") { try { cb(payload); } catch {} }
612
+ } catch {}
613
+ }, ms);
614
+ info(`[proactive] 主动任务生成已启动 (每 ${Math.round(ms / 60000)} 分钟)`);
615
+ return this._proactiveTimer;
616
+ }
617
+
618
+ stopProactiveTicker() {
619
+ if (this._proactiveTimer) { clearInterval(this._proactiveTimer); this._proactiveTimer = null; }
620
+ }
621
+
622
+ // 接入 MCP 服务器: 连接 + 注册工具到 catalog (可选能力, 显式调用)
623
+ // servers 缺省用 config.mcp.servers; 返回注册的工具数
624
+ async connectMcp(servers = null) {
625
+ const list = servers || (this.config.mcp && this.config.mcp.servers) || [];
626
+ if (!list.length) return 0;
627
+ const r = await registerMcpTools(this.tools, list);
628
+ this._mcp = r;
629
+ return r.count;
630
+ }
631
+
632
+ // 热重载提供方: 从 config/ppx.json 重建 LLM 客户端列表
633
+ // 用法: HTTP API 增删改提供方后调用, 立即生效无需重启
634
+ // provider 可用性解析复用 plugin/builtin.js (只此一份, 不重复实现)
635
+ // 发布首启引导: 若未配置任何云端大模型 API key, 明确提示按 README 配置 (不阻断运行)
636
+ _warnMissingCloudApi() {
637
+ const provs = (this.config && this.config.providers) || [];
638
+ const isLocal = (p) => /127\.0\.0\.1|localhost|lm-studio|ollama/i.test(p.base_url || "");
639
+ const isSpecial = (p) => p.backend === "openclaw" || p.backend === "dsh" || p.id === "dsh";
640
+ const cloud = provs.filter((p) => !isLocal(p) && !isSpecial(p));
641
+ const hasCloudKey = cloud.some((p) => p.api_key || (p.api_key_env && process.env[p.api_key_env]));
642
+ const hasLocal = provs.some(isLocal);
643
+ // 默认本地优先(agent.model_preference=local): 有本地模型即满足运行, 不告警(本地测试正用本地模型)
644
+ if (hasLocal) return;
645
+ // 无本地但配了云端 key → 正常走云端, 不告警
646
+ if (hasCloudKey) return;
647
+ // 任何可用模型都没有 → 必须提示, 否则对话不可用
648
+ warn(
649
+ "未检测到任何可用模型。皮皮虾默认本地优先(agent.model_preference=local), 需至少满足一项:\n" +
650
+ " 1) 启动本地模型服务 (LM Studio 等, 127.0.0.1 即识别)\n" +
651
+ " 2) 配置云端大模型 API key: OPENAI_API_KEY | DEEPSEEK_API_KEY | VOLCENGINE_API_KEY(需填 endpoint) | DASHSCOPE_API_KEY\n" +
652
+ " 3) 或显式设 agent.model_preference=cloud 后走云端 key\n" +
653
+ "详见 README「快速开始」模型接入节。"
654
+ );
655
+ }
656
+ // 热重载提供方: 从 config/ppx.json 重建 LLM 客户端列表
657
+ reloadProviders() {
658
+ this.config = this._loadConfig(null);
659
+ this.llm = resolveLLM(this.config);
660
+ this.allProviders = resolveAllLLMs(this.config);
661
+ info(`[providers] 热重载完成: ${this.allProviders.length} 个客户端`);
662
+ return { llm: this.llm ? (this.llm.backend || this.llm.model) : null, count: this.allProviders.length };
663
+ }
664
+
665
+ // 热重载通用设置 (用户名/安全/agent 预设): 重新加载 config + 更新内存字段
666
+ // 不重建 LLM 客户端 (settings 不涉及 provider), 只让 user.name/security/agent 生效
667
+ reloadSettings() {
668
+ this.config = this._loadConfig(null);
669
+ this.userName = this.config.user?.name || "兄弟";
670
+ this.ctx.provide("userName", this.userName);
671
+ // 应用 tools.disabled 变更 (设置 UI 启停工具后立即生效)
672
+ this._applyDisabledTools();
673
+ info(`[settings] 热重载完成: 用户=${this.userName}, 模式=${this.config.agent?.mode || "react"}`);
674
+ return { userName: this.userName, mode: this.config.agent?.mode || "react" };
675
+ }
676
+
677
+ // 应用 config.tools.disabled: 禁用列表中的工具 (启停工具 UI 写盘后生效)
678
+ _applyDisabledTools() {
679
+ try {
680
+ const disabled = Array.isArray(this.config.tools?.disabled) ? this.config.tools.disabled : [];
681
+ if (!disabled.length) return;
682
+ let n = 0;
683
+ for (const name of disabled) {
684
+ try { if (this.tools.disable(name)) n += 1; } catch {}
685
+ }
686
+ if (n) info(`[tools] 已禁用 ${n} 个工具: ${disabled.join(", ")}`);
687
+ } catch { /* 工具未就绪时静默跳过 */ }
688
+ }
689
+
690
+ shutdown() {
691
+ this.stopProactiveTicker();
692
+ this._mcp?.close?.();
693
+ // v1.0.8: 清理军团子进程 (spawn_agent 派生的 worker), 防后台残留
694
+ if (this._legion && typeof this._legion.shutdownAll === "function") {
695
+ try { this._legion.shutdownAll(); } catch {}
696
+ }
697
+ this.memory._saveState?.();
698
+ this.scheduler?.shutdown?.(); // ⑤/②: 清定时器防进程挂起
699
+ this.healer.markClean();
700
+ }
701
+ }
702
+
703
+ // 重构 (2026-09-15): 历史/上下文管理 + 提示词构建以 mixin 方式挂回 prototype
704
+ // (行为与拆前完全一致, 实例方法与调用方不受影响; 测试走 agent._xxx 不感知拆分)
705
+ Object.assign(PPXAgent.prototype, contextMethods, promptMethods);
706
+
707
+ ensureUTF8Console();
708
+ if (process.argv[1] && process.argv[1].endsWith("src/agent/index.js")) {
709
+ const agent = new PPXAgent();
710
+ console.log(`皮皮虾 就绪 | 记忆:${agent.facts.count()}条 | 工具:${agent.tools.list().join(",")} | 自愈:${agent.health.fixes.length ? "修复" + agent.health.fixes.length + "项" : "OK"}`);
711
+ process.stdin.on("data", async (d) => {
712
+ const line = d.toString().trim();
713
+ if (["quit", "exit"].includes(line)) { agent.shutdown(); process.exit(0); }
714
+ const r = await agent.chat(line);
715
+ console.log("\n" + r + "\n");
716
+ });
717
+ }