ppxans-harness 2.4.0 → 3.2.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 (186) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +218 -265
  3. package/bin/ppx-channels.js +2 -2
  4. package/bin/ppx-serve.js +5 -5
  5. package/bin/ppx-setup.js +124 -0
  6. package/bin/ppx-web.js +140 -0
  7. package/bin/ppx.js +2 -2
  8. package/config/identity.md +6 -6
  9. package/config/ishiki.md +16 -16
  10. package/config/ppx.json +15 -151
  11. package/config/ppx.json.example +143 -0
  12. package/package.json +17 -10
  13. package/skills/.usage.json +6 -0
  14. package/skills/agent-professional-training/SKILL.md +94 -0
  15. package/skills/brainstorm/SKILL.md +24 -0
  16. package/skills/cupid-lover-comms/SKILL.md +37 -0
  17. package/skills/debug/SKILL.md +26 -0
  18. package/skills/plan/SKILL.md +25 -0
  19. package/skills/ponytail/SKILL.md +25 -0
  20. package/skills/ppx-memory/SKILL.md +91 -0
  21. package/skills/ppx-memory/scripts/cli.js +192 -0
  22. package/skills/ppx-memory/scripts/experience.js +133 -0
  23. package/skills/ppx-memory/scripts/fact-store.js +842 -0
  24. package/skills/ppx-memory/scripts/l0.js +52 -0
  25. package/skills/ppx-memory/scripts/l2.js +146 -0
  26. package/skills/ppx-memory/scripts/l3.js +112 -0
  27. package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
  28. package/skills/ppx-memory/scripts/pii.js +42 -0
  29. package/skills/ppx-memory/scripts/schema.js +80 -0
  30. package/skills/ppx-memory/scripts/session.js +398 -0
  31. package/skills/ppx-memory/scripts/similarity.js +43 -0
  32. package/skills/ppx-memory/scripts/store.js +116 -0
  33. package/skills/ppx-memory/scripts/wal.js +38 -0
  34. package/skills/ppx-selfheal/SKILL.md +24 -0
  35. package/skills/ppx-selfheal/scripts/cli.js +80 -0
  36. package/skills/ppx-selfheal/scripts/healer.js +184 -0
  37. package/skills/ppx-selfheal/scripts/logger.js +17 -0
  38. package/skills/ppx-selfheal/scripts/store.js +116 -0
  39. package/skills/prompt-depth-kit/SKILL.md +28 -0
  40. package/skills/session-naming/SKILL.md +36 -0
  41. package/skills/verify/SKILL.md +25 -0
  42. package/src/agent/index.js +1340 -717
  43. package/src/agent/prompts.js +47 -3
  44. package/src/aml-server.js +197 -151
  45. package/src/ans/eviction.js +123 -143
  46. package/src/ans/guard.js +159 -120
  47. package/src/ans/lifecycle.js +96 -93
  48. package/src/ans/proactive.js +112 -129
  49. package/src/ans/reward.js +95 -111
  50. package/src/ans/values.js +15 -15
  51. package/src/audit/audit-chain.js +43 -7
  52. package/src/audit/verifier.js +157 -120
  53. package/src/bus/circuit-breaker.js +9 -1
  54. package/src/bus/runtime-bus.js +107 -93
  55. package/src/channels/base.js +57 -34
  56. package/src/channels/feishu.js +118 -126
  57. package/src/channels/http.js +1090 -592
  58. package/src/channels/index.js +111 -110
  59. package/src/channels/log.js +29 -29
  60. package/src/channels/wechat-crypto.js +73 -74
  61. package/src/channels/wechat.js +191 -197
  62. package/src/channels/workspace.js +94 -0
  63. package/src/channels-cli.js +126 -124
  64. package/src/cli.js +129 -120
  65. package/src/commands/index.js +142 -0
  66. package/src/config/channels.js +137 -170
  67. package/src/config/index.js +277 -224
  68. package/src/config/placeholder.js +31 -0
  69. package/src/config/providers.js +148 -188
  70. package/src/config/settings.js +156 -182
  71. package/src/core/policy.js +69 -18
  72. package/src/core/trace.js +8 -6
  73. package/src/edit/editblock.js +266 -0
  74. package/src/edit/snapshot.js +67 -0
  75. package/src/evidence/index.js +153 -0
  76. package/src/evolve/playbook.js +9 -10
  77. package/src/hooks/index.js +113 -0
  78. package/src/llm/client.js +188 -446
  79. package/src/llm/dsml.js +74 -74
  80. package/src/llm/embedder.js +41 -35
  81. package/src/llm/fence.js +52 -105
  82. package/src/llm/index.js +4 -4
  83. package/src/llm/local-embedder.js +94 -0
  84. package/src/llm/presets.js +113 -0
  85. package/src/llm/pricing.js +93 -0
  86. package/src/llm/retry.js +73 -73
  87. package/src/llm/router.js +92 -97
  88. package/src/mcp/admin.js +326 -0
  89. package/src/mcp/client.js +487 -375
  90. package/src/mcp/http.js +203 -0
  91. package/src/mcp/index.js +116 -116
  92. package/src/mcp/server.js +392 -0
  93. package/src/mcp/tasks.js +133 -0
  94. package/src/memory/asset-hub.js +6 -11
  95. package/src/memory/canvas.js +2 -5
  96. package/src/memory/compaction.js +28 -28
  97. package/src/memory/experience.js +133 -122
  98. package/src/memory/fact-store.js +914 -698
  99. package/src/memory/failure-episode.js +20 -11
  100. package/src/memory/fork.js +17 -8
  101. package/src/memory/index.js +8 -6
  102. package/src/memory/l0.js +53 -52
  103. package/src/memory/l2.js +145 -130
  104. package/src/memory/l3.js +111 -111
  105. package/src/memory/legion-board.js +71 -0
  106. package/src/memory/memory-ticker.js +239 -240
  107. package/src/memory/session.js +398 -397
  108. package/src/memory/sqlite-store.js +581 -0
  109. package/src/mode/blackboard.js +49 -49
  110. package/src/mode/graph.js +42 -41
  111. package/src/mode/index.js +64 -64
  112. package/src/mode/legion.js +54 -51
  113. package/src/mode/plan-exec.js +50 -50
  114. package/src/mode/router.js +28 -40
  115. package/src/orchestrator/agent-worker.js +69 -69
  116. package/src/orchestrator/dag.js +90 -83
  117. package/src/orchestrator/experts.js +76 -0
  118. package/src/orchestrator/index.js +1 -1
  119. package/src/orchestrator/legion.js +179 -187
  120. package/src/orchestrator/supervisor.js +6 -8
  121. package/src/permissions/index.js +378 -0
  122. package/src/persona/index.js +28 -29
  123. package/src/plugin/builtin.js +313 -212
  124. package/src/plugin/context.js +80 -79
  125. package/src/plugin/index.js +62 -62
  126. package/src/plugin/v3.js +73 -0
  127. package/src/protocol/index.js +148 -0
  128. package/src/repomap/index.js +309 -0
  129. package/src/review/index.js +393 -0
  130. package/src/seam/registry.js +3 -0
  131. package/src/seam/shell.js +55 -55
  132. package/src/security/injection.js +79 -0
  133. package/src/selfheal/evolve.js +67 -67
  134. package/src/selfheal/healer.js +184 -167
  135. package/src/selfheal/run.js +9 -9
  136. package/src/server.js +63 -60
  137. package/src/services/diagnose.js +180 -0
  138. package/src/services/learning-service.js +9 -0
  139. package/src/services/memory-health.js +34 -6
  140. package/src/services/memory-service.js +42 -9
  141. package/src/services/triage.js +138 -0
  142. package/src/session/parts.js +76 -0
  143. package/src/session/projection.js +73 -0
  144. package/src/session/rollout.js +54 -0
  145. package/src/session/turn.js +137 -0
  146. package/src/skills/lint.js +72 -0
  147. package/src/skills/loader.js +231 -150
  148. package/src/skills/search.js +58 -0
  149. package/src/skills/verify.js +95 -100
  150. package/src/tools/advanced.js +388 -352
  151. package/src/tools/builtin.js +384 -297
  152. package/src/tools/catalog.js +283 -159
  153. package/src/tools/command-guard.js +112 -112
  154. package/src/tools/custom.js +47 -47
  155. package/src/tools/delegate.js +383 -297
  156. package/src/tools/document.js +254 -253
  157. package/src/tools/git.js +151 -0
  158. package/src/tools/governance.js +47 -20
  159. package/src/tools/index.js +16 -11
  160. package/src/tools/methods.js +178 -178
  161. package/src/tools/ocr.js +59 -59
  162. package/src/tools/sandbox-worker.js +40 -0
  163. package/src/tools/sandbox.js +92 -0
  164. package/src/tools/seam.js +162 -125
  165. package/src/tools/selfmod.js +196 -176
  166. package/src/tools/v3.js +225 -0
  167. package/src/tools/vad.js +176 -0
  168. package/src/tools/voice.js +238 -0
  169. package/src/utils/async.js +14 -0
  170. package/src/utils/config-file.js +53 -0
  171. package/src/utils/crashguard.js +88 -0
  172. package/src/utils/http.js +53 -0
  173. package/src/utils/id.js +8 -0
  174. package/src/utils/json-state.js +33 -0
  175. package/src/utils/logger.js +17 -17
  176. package/src/utils/ndjson.js +25 -0
  177. package/src/utils/pii.js +42 -42
  178. package/src/utils/rate-limit.js +50 -0
  179. package/src/utils/schema.js +80 -0
  180. package/src/utils/similarity.js +43 -0
  181. package/src/utils/store.js +170 -108
  182. package/src/utils/text.js +15 -15
  183. package/src/utils/trace.js +153 -153
  184. package/src/utils/wal.js +39 -0
  185. package/src/utils/winutf8.js +16 -15
  186. package/src/wiki/index.js +170 -0
@@ -0,0 +1,180 @@
1
+ // src/services/diagnose.js - Agent 自诊断 (2026-10-03, "按症状下药"表自动化)
2
+ // 数据源全部为本地白盒: 审计哈希链 (工具调用 ok/error/ms) + 失败案例库 + 使用统计 + 任务基线。
3
+ // 输出: 症状 → 根因 → 增强动作 (对齐强化框架的按症状下药表), 零 LLM 参与, 确定性可复现。
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { auditFile } from "../audit/audit-chain.js";
7
+ import { summarizeCauses } from "./triage.js";
8
+ import { debug } from "../utils/logger.js";
9
+
10
+ function readJson(p, fallback) {
11
+ try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return fallback; }
12
+ }
13
+
14
+ // 读审计链最近 maxLines 条, 聚合出工具级信号
15
+ function readAuditSignals(dataDir, maxLines = 5000) {
16
+ const file = auditFile(dataDir);
17
+ let lines = [];
18
+ try { lines = fs.readFileSync(file, "utf8").trim().split("\n").filter(Boolean); } catch { return null; }
19
+ lines = lines.slice(-maxLines);
20
+ const calls = [];
21
+ for (const line of lines) {
22
+ try { calls.push(JSON.parse(line)); } catch (e) { debug(`[services/diagnose] 已忽略异常: ${e && e.message ? e.message : e}`); }
23
+ }
24
+ if (!calls.length) return null;
25
+ const byTool = new Map(); // tool -> {calls, fails, errors:{}}
26
+ let paramErrors = 0, policyBlocks = 0, timeouts = 0;
27
+ for (const c of calls) {
28
+ const t = byTool.get(c.tool) || { calls: 0, fails: 0 };
29
+ t.calls++;
30
+ if (!c.ok) {
31
+ t.fails++;
32
+ const err = String(c.error || "");
33
+ if (err.includes("参数错误")) paramErrors++;
34
+ else if (err.includes("策略拦截") || err.includes("权限") || err.includes("审批")) policyBlocks++;
35
+ else if (/超时|timeout/i.test(err)) timeouts++;
36
+ }
37
+ byTool.set(c.tool, t);
38
+ }
39
+ return { file, total: calls.length, byTool, paramErrors, policyBlocks, timeouts };
40
+ }
41
+
42
+ export function diagnoseAgent({ dataDir, rootDir = null, usageStats = null, health = null, topPerFinding = 3 } = {}) {
43
+ const findings = [];
44
+ const signals = {};
45
+
46
+ // 1. 审计链信号
47
+ const audit = readAuditSignals(dataDir);
48
+ signals.audit = audit ? { total: audit.total, paramErrors: audit.paramErrors, policyBlocks: audit.policyBlocks, timeouts: audit.timeouts } : null;
49
+ if (audit) {
50
+ // 症状: 工具调用错 (参数类)
51
+ if (audit.paramErrors > 0) {
52
+ findings.push({
53
+ severity: "info",
54
+ symptom: `参数校验器拦截了 ${audit.paramErrors} 次错误参数 (未浪费 LLM 轮次)`,
55
+ rootCause: "模型传参不规范 / 工具 schema 描述不够清晰",
56
+ action: "参数校验已生效; 若同一工具反复被拦, 优先完善该工具的参数描述与示例",
57
+ });
58
+ }
59
+ // 症状: 权限拦截频繁
60
+ if (audit.policyBlocks >= 3) {
61
+ findings.push({
62
+ severity: "medium",
63
+ symptom: `权限/策略拦截 ${audit.policyBlocks} 次`,
64
+ rootCause: "权限配置与当前任务错配 (最小权限不等于处处设卡)",
65
+ action: "用 PERMISSION_PRESETS 切换沙箱档位, 或对确需的高频工具 addRule allow (opencode 规则链)",
66
+ });
67
+ }
68
+ // 症状: 单工具失败率异常
69
+ for (const [tool, t] of audit.byTool) {
70
+ if (t.calls >= 5 && t.fails / t.calls > 0.3) {
71
+ findings.push({
72
+ severity: "high",
73
+ symptom: `工具 ${tool} 失败率 ${(100 * t.fails / t.calls).toFixed(0)}% (${t.fails}/${t.calls})`,
74
+ rootCause: "工具 schema 不清晰 / 实现缺陷 / 上游依赖不稳",
75
+ action: `优先修复 ${tool}: 补参数描述与示例 + 失败重试 (idempotent) + 结果验证器`,
76
+ });
77
+ }
78
+ }
79
+ // 症状: 超时
80
+ if (audit.timeouts >= 2) {
81
+ findings.push({
82
+ severity: "medium",
83
+ symptom: `工具超时 ${audit.timeouts} 次`,
84
+ rootCause: "上游依赖不稳 / timeoutMs 配置过紧",
85
+ action: "对幂等工具启用重试; 检查 timeoutMs 与上游可用性",
86
+ });
87
+ }
88
+ }
89
+
90
+ // 2. 失败案例库
91
+ const episodes = readJson(path.join(dataDir, "memory", "failures", "episodes.json"), []);
92
+ signals.failureEpisodes = episodes.length;
93
+ if (episodes.length >= 3) {
94
+ findings.push({
95
+ severity: "medium",
96
+ symptom: `失败案例库 ${episodes.length} 条 (近端: ${episodes.slice(0, topPerFinding).map((e) => e.tool).join(", ")})`,
97
+ rootCause: "同类失败反复出现 = 系统性短板, 非偶发",
98
+ action: "对高频失败做归因分类 (知识/工具/规划/验证/记忆), 逐类补齐; 每次修复后跑回归",
99
+ });
100
+ }
101
+
102
+ // 2b. 记忆管线健康度 (2026-10-03 接线: MemoryHealthMonitor 曾被装配却零消费)
103
+ if (health) {
104
+ signals.memoryHealth = {
105
+ overall: health.overall,
106
+ worstRecentFails: health.worstRecentFails,
107
+ totalFail: health.totalFail,
108
+ };
109
+ if (health.overall && health.overall !== "healthy") {
110
+ const badSteps = (health.steps || [])
111
+ .filter((s) => s.recentFails > 0)
112
+ .map((s) => `${s.name}(${s.recentFails}次: ${String(s.lastError || "?").slice(0, 60)})`);
113
+ findings.push({
114
+ severity: health.overall === "unhealthy" ? "high" : "medium",
115
+ symptom: `记忆管线 ${health.overall} — 窗口内最差单步失败 ${health.worstRecentFails} 次, 累计 ${health.totalFail} 次`,
116
+ rootCause: badSteps.length ? badSteps.join("; ") : "上游模型或存储不稳",
117
+ action: health.overall === "unhealthy"
118
+ ? "已强制降级为「只写不压」(跳过 compact/extract); 先排查 LLM provider 可用性与磁盘写入权限"
119
+ : "观察是否持续恶化; 可用 model_routing.aux 把辅助任务切到更稳/更便宜的模型",
120
+ });
121
+ }
122
+ }
123
+
124
+ // 3. 使用统计 (成本症状)
125
+ const usage = usageStats || readJson(path.join(dataDir, "usage-stats.json"), null);
126
+ signals.usage = usage;
127
+ if (usage && usage.calls >= 5) {
128
+ const costPerCall = Math.round(usage.tokens / usage.calls);
129
+ if (costPerCall > 15000) {
130
+ findings.push({
131
+ severity: "medium",
132
+ symptom: `单次调用平均 ${costPerCall} tok (共 ${usage.calls} 次)`,
133
+ rootCause: "全用大模型 / 上下文冗余",
134
+ action: "启用 model_routing.aux 分层路由 (辅助任务走便宜模型) + 检查上下文压缩是否生效",
135
+ });
136
+ }
137
+ }
138
+
139
+ // 4. 任务基线 (规划症状): 只读显式提供的 rootDir, 不回落 cwd (避免跨仓库污染诊断)
140
+ let baseline = readJson(path.join(dataDir, "baseline.json"), null);
141
+ if (!baseline && rootDir) baseline = readJson(path.join(rootDir, "bench", "baseline.json"), null);
142
+ if (baseline && Array.isArray(baseline.results)) {
143
+ const fails = baseline.results.filter((r) => !r.pass);
144
+ signals.baseline = { coverage: `${baseline.results.length} 任务`, failures: fails.length };
145
+ if (fails.length) {
146
+ // 失败归因 (2026-10-03 接线): 基线里若带 triage 记录, 直接给确定性根因与处方;
147
+ // 没有才回落到"需人工判定" —— 这正是框架第 1 条要消灭的那句话。
148
+ const triages = fails.map((f) => f.triage).filter(Boolean);
149
+ const causes = triages.length ? summarizeCauses(triages) : null;
150
+ findings.push({
151
+ severity: "high",
152
+ symptom: `任务基线有 ${fails.length} 项未通过: ${fails.slice(0, topPerFinding).map((f) => f.id).join(", ")}`,
153
+ rootCause: causes
154
+ ? `失败归因: ${Object.entries(causes.byCause).sort((a, b) => b[1] - a[1]).map(([c, n]) => `${c}×${n}`).join(", ")}`
155
+ : "未归因 (重跑 taskbench 可自动归因)",
156
+ action: causes?.dominantAction || "任务分解 + 验证器 + 重规划; 先人工复跑确认是真实失败还是环境误伤",
157
+ evidence: triages.flatMap((t) => t.evidence || []).slice(0, 4),
158
+ });
159
+ }
160
+ }
161
+
162
+ if (!findings.length) {
163
+ findings.push({ severity: "ok", symptom: "未检出系统性短板", rootCause: "各信号均在阈值内", action: "保持既有节奏: 跑基准 → 找短板 → 优化 → 对比" });
164
+ }
165
+
166
+ // 报告渲染
167
+ const L = ["═══ 皮皮虾自诊断报告 ═══", ""];
168
+ L.push(`审计信号: ${signals.audit ? `${signals.audit.total} 次工具调用 (参数错 ${signals.audit.paramErrors} / 策略拦 ${signals.audit.policyBlocks} / 超时 ${signals.audit.timeouts})` : "(无审计数据)"}`);
169
+ L.push(`失败案例: ${signals.failureEpisodes} 条 | 使用: ${signals.usage ? `${signals.usage.calls} 次 / ${signals.usage.tokens} tok` : "(无数据)"}`);
170
+ L.push(`记忆管线: ${signals.memoryHealth ? `${signals.memoryHealth.overall} (最差步失败 ${signals.memoryHealth.worstRecentFails} 次 / 累计 ${signals.memoryHealth.totalFail})` : "(未接入)"}`);
171
+ L.push(`任务基线: ${signals.baseline ? `${signals.baseline.coverage}, ${signals.baseline.failures} 项未通过` : "(未建立)"}`);
172
+ L.push("");
173
+ for (const f of findings) {
174
+ L.push(`[${f.severity.toUpperCase()}] ${f.symptom}`);
175
+ L.push(` 根因: ${f.rootCause}`);
176
+ L.push(` 动作: ${f.action}`);
177
+ L.push("");
178
+ }
179
+ return { report: L.join("\n").trim(), findings, signals };
180
+ }
@@ -28,6 +28,8 @@ export class LearningService {
28
28
  this.tracer = deps.tracer; // 结构化事件流
29
29
  this.toolNames = deps.toolNames; // () => string[], 工具名清单 (verifyLesson 接地)
30
30
  this.runTool = deps.runTool; // (name, args) => Promise<string>, create_skill 调用
31
+ // 2026-10-03 接线: 语境 Playbook —— refine 成功的教训同步沉淀为 playbook bullet (反哺 system prompt)
32
+ this.playbook = deps.playbook || null;
31
33
  if (!(this.auditor instanceof Auditor)) {
32
34
  // 兼容: 外部可能传普通对象 (测试/轻量装配), 不强求 Auditor 实例
33
35
  }
@@ -63,6 +65,13 @@ export class LearningService {
63
65
  (p) => {
64
66
  this.experience.learn({ task: "自动提炼", lesson: p.lesson, tags: ["auto-refine"] });
65
67
  if (this.lifecycle) this.lifecycle.evolve(); // 生命周期: 进化计数 (落盘)
68
+ // 2026-10-03 接线: 教训 → playbook bullet (grow-and-refine 去重内置; 空库首次 ADD)
69
+ if (this.playbook) {
70
+ try {
71
+ this.playbook.apply([{ op: "ADD", kind: "lesson", content: p.lesson, evidence_ref: "learning/refine" }]);
72
+ this.tracer?.event("evolve/playbook", { op: "ADD", lesson: p.lesson.slice(0, 120) });
73
+ } catch (e) { /* playbook 沉淀失败不影响经验库主链路 */ }
74
+ }
66
75
  }
67
76
  );
68
77
  if (!g.committed) {
@@ -7,11 +7,17 @@
7
7
  export const HEALTH = { HEALTHY: "healthy", DEGRADED: "degraded", UNHEALTHY: "unhealthy" };
8
8
 
9
9
  export class MemoryHealthMonitor {
10
- constructor({ degradeAfter = 3, windowMs = 5 * 60 * 1000 } = {}) {
10
+ constructor({ degradeAfter = 3, unhealthyAfter = null, windowMs = 5 * 60 * 1000 } = {}) {
11
11
  this.degradeAfter = degradeAfter; // 连续失败多少次进入 degraded
12
+ // 修复 (2026-09-17): unhealthy 原先不可达 (status() 三元两分支都返回 HEALTHY)。
13
+ // 现按窗口内失败数分为两档: >= degradeAfter → degraded, >= unhealthyAfter → unhealthy。
14
+ // 默认取 degradeAfter 的两倍, 保持"降级先发生、重度降级随后"的渐进语义。
15
+ this.unhealthyAfter = Number(unhealthyAfter) || degradeAfter * 2;
12
16
  this.windowMs = windowMs; // 统计窗口 (默认 5 分钟)
13
17
  this.steps = new Map(); // stepName -> { ok, fail, lastError, lastAt, fails: [] }
14
18
  this.degraded = false; // 降级开关 (只写不压)
19
+ this.unhealthy = false; // 重度降级 (持续失败)
20
+ this._worst = 0; // 窗口内最差单步失败数 (可观测)
15
21
  }
16
22
 
17
23
  _step(name) {
@@ -40,7 +46,7 @@ export class MemoryHealthMonitor {
40
46
  return s;
41
47
  }
42
48
 
43
- // 连续失败 -> degraded (滑动窗口内失败数 >= degradeAfter)
49
+ // 连续失败 -> degraded / unhealthy (滑动窗口内失败数分档)
44
50
  _recompute() {
45
51
  const now = Date.now();
46
52
  let worst = 0;
@@ -48,7 +54,9 @@ export class MemoryHealthMonitor {
48
54
  const recent = s.fails.filter((t) => now - t < this.windowMs).length;
49
55
  if (recent > worst) worst = recent;
50
56
  }
57
+ this._worst = worst;
51
58
  this.degraded = worst >= this.degradeAfter;
59
+ this.unhealthy = worst >= this.unhealthyAfter;
52
60
  return this.degraded;
53
61
  }
54
62
 
@@ -78,17 +86,37 @@ export class MemoryHealthMonitor {
78
86
  recentFails: s.fails.filter((t) => now - t < this.windowMs).length,
79
87
  }));
80
88
  const totalFail = stepList.reduce((a, s) => a + s.fail, 0);
81
- const overall = this.degraded
82
- ? HEALTH.DEGRADED
83
- : (totalFail === 0 ? HEALTH.HEALTHY : HEALTH.HEALTHY);
84
- return { overall, degraded: this.degraded, steps: stepList, updatedAt: now };
89
+ // 三态可达 (修复 2026-09-17): unhealthy > degraded > healthy
90
+ const overall = this.unhealthy
91
+ ? HEALTH.UNHEALTHY
92
+ : (this.degraded ? HEALTH.DEGRADED : HEALTH.HEALTHY);
93
+ return {
94
+ overall,
95
+ degraded: this.degraded,
96
+ unhealthy: this.unhealthy,
97
+ worstRecentFails: this._worst,
98
+ thresholds: { degradeAfter: this.degradeAfter, unhealthyAfter: this.unhealthyAfter, windowMs: this.windowMs },
99
+ totalFail,
100
+ steps: stepList,
101
+ updatedAt: now,
102
+ };
85
103
  }
86
104
 
87
105
  // 降级建议 (供 memory ticker / agent 消费)
88
106
  advice() {
89
107
  if (!this.degraded) return { action: "normal" };
108
+ if (this.unhealthy) {
109
+ return {
110
+ action: "degrade",
111
+ severe: true,
112
+ reason: `记忆管线持续失败 (窗口内 ${this._worst} 次, 已达重度阈值 ${this.unhealthyAfter}), 强制降级为只写不压`,
113
+ // 只写不压: 跳过 LLM 压缩/提炼, 仅保留原始会话写入
114
+ skip: ["compact", "extract"],
115
+ };
116
+ }
90
117
  return {
91
118
  action: "degrade",
119
+ severe: false,
92
120
  reason: "记忆管线连续失败, 建议降级为只写不压",
93
121
  // 只写不压: 跳过 LLM 压缩/提炼, 仅保留原始会话写入
94
122
  skip: ["compact", "extract"],
@@ -23,11 +23,22 @@ export class MemoryService {
23
23
  this.experience = deps.experience; // 经验库
24
24
  this.lifecycle = deps.lifecycle; // ANS 生命周期 (进化计数)
25
25
  this.tracer = deps.tracer; // 结构化事件流
26
+ // 记忆管线健康监控 (2026-10-03 接线): MemoryHealthMonitor 由 evolvePlugin 装配,
27
+ // 但此前**零消费** —— 没人 record() 也没人读 advice(), 于是 status() 永远是 healthy,
28
+ // 降级逻辑形同虚设。现由本服务在每步管线记录成败, 并在降级时真的跳过 LLM 压缩/提炼。
29
+ this.health = deps.health || null;
26
30
  this._personaBuilt = null; // L3 画像上次生成日期 (跨天刷新标记, 原 agent 字段)
27
31
  }
28
32
 
29
33
  _llm() { return this.getLlm ? this.getLlm() : null; }
30
34
 
35
+ // 管线步骤门禁: 健康度降级时跳过对应 LLM 步骤 (只写不压 → 防上下文污染 + 省调用)
36
+ _skipStep(step) {
37
+ if (!this.health || typeof this.health.advice !== "function") return false;
38
+ const adv = this.health.advice();
39
+ return !!(adv && Array.isArray(adv.skip) && adv.skip.includes(step));
40
+ }
41
+
31
42
  // 辅助 LLM 调用前置健康探测: 模型不可用 (本地服务未运行/远端不可达) 时快速跳过
32
43
  async _auxLlmReady() {
33
44
  const llm = this._llm();
@@ -40,6 +51,10 @@ export class MemoryService {
40
51
  async extractMemory(user, assistant, existing = []) {
41
52
  const llm = this._llm();
42
53
  if (!llm) return [];
54
+ if (this._skipStep("extract")) {
55
+ this.tracer?.event("memory/extract", { count: 0, reason: "health_degraded" });
56
+ return [];
57
+ }
43
58
  if (!(await this._auxLlmReady())) return []; // 模型不可用时跳过提炼 (退回启发式)
44
59
  // 噪声治理: 显式跳过寒暄/无信息量/关于系统本身的元讨论
45
60
  const sys = "你是记忆提炼器。从对话中提取值得长期记忆的关键事实、用户偏好、待办事项。只输出 JSON 数组, 每项是{content: 一句完整中文记忆}。没有值得记的返回 []。不要解释, 只输出 JSON。\n跳过以下内容: 1) 寒暄/问候/客套话; 2) 无信息量的闲聊; 3) 对助手/系统本身的元讨论与建议 (如任务描述方式、提示词建议等); 4) 已被现有记忆覆盖的内容。";
@@ -49,10 +64,18 @@ export class MemoryService {
49
64
  userMsg += "\n\n【已有记忆】以下记忆已存在, 若你提炼的内容与其中任意一条含义相同或被其覆盖, 则不要输出该条 (避免重复):\n"
50
65
  + existing.map((f, i) => `${i + 1}. ${f.content}`).join("\n");
51
66
  }
52
- const r = await llm.chat([
53
- { role: "system", content: sys },
54
- { role: "user", content: userMsg },
55
- ], { timeoutMs: AUX_LLM_TIMEOUT_MS, retryMax: 0 });
67
+ let r;
68
+ try {
69
+ r = await llm.chat([
70
+ { role: "system", content: sys },
71
+ { role: "user", content: userMsg },
72
+ ], { timeoutMs: AUX_LLM_TIMEOUT_MS, retryMax: 0 });
73
+ this.health?.record("extract", { ok: true });
74
+ } catch (e) {
75
+ // 管线步骤成败必须记账 —— 没有这个信号, MemoryHealthMonitor 永远停在 healthy
76
+ this.health?.record("extract", { ok: false, error: e?.message || String(e) });
77
+ throw e; // 保持原语义: 由调用方决定是否降级
78
+ }
56
79
  const text = String(r.content || "").trim();
57
80
  // 容忍模型把 JSON 包在 markdown 代码块里
58
81
  const cleaned = text.replace(/```(?:json|JSON)?\s*/g, "").replace(/```/g, "").trim();
@@ -71,11 +94,19 @@ export class MemoryService {
71
94
  async summarizeMemory(raw) {
72
95
  const llm = this._llm();
73
96
  if (!llm) throw new Error("无 LLM");
97
+ if (this._skipStep("compact")) throw new Error("记忆管线降级中, 跳过压缩 (只写不压)");
74
98
  if (!(await this._auxLlmReady())) throw new Error("LLM 不可用, 跳过辅助摘要");
75
- const r = await llm.chat([
76
- { role: "system", content: "你是记忆压缩器。把下面这段对话记录压缩成一段简洁的中文摘要(≤200字), 保留关键事实、用户偏好、进展和待办。不要客套, 直接输出摘要。" },
77
- { role: "user", content: String(raw).slice(0, 4000) },
78
- ], { timeoutMs: AUX_LLM_TIMEOUT_MS, retryMax: 0 });
99
+ let r;
100
+ try {
101
+ r = await llm.chat([
102
+ { role: "system", content: "你是记忆压缩器。把下面这段对话记录压缩成一段简洁的中文摘要(≤200字), 保留关键事实、用户偏好、进展和待办。不要客套, 直接输出摘要。" },
103
+ { role: "user", content: String(raw).slice(0, 4000) },
104
+ ], { timeoutMs: AUX_LLM_TIMEOUT_MS, retryMax: 0 });
105
+ this.health?.record("compact", { ok: true });
106
+ } catch (e) {
107
+ this.health?.record("compact", { ok: false, error: e?.message || String(e) });
108
+ throw e;
109
+ }
79
110
  this.tracer?.event("memory/summarize", { chars: String(raw).length, ok: true });
80
111
  return r.content;
81
112
  }
@@ -101,7 +132,9 @@ export class MemoryService {
101
132
  // 有 embedder 时走 dense 语义检索 (与 BM25 RRF 融合), 否则 LLM 扩展 + RRF
102
133
  let hits;
103
134
  if (this.facts.embedder) {
104
- hits = this.facts.querySemantic(q, { limit, scope });
135
+ // 2026-09-18 修复: querySemantic 是 async, 原漏 await 导致 hits 是 Promise,
136
+ // tracer 恒记 hits:0 (可观测数据失真), 后续读 hits 也会踩坑
137
+ hits = await this.facts.querySemantic(q, { limit, scope });
105
138
  } else {
106
139
  const variants = [q];
107
140
  const llm = this._llm();
@@ -0,0 +1,138 @@
1
+ // src/services/triage.js - 失败归因 (Agent 增强框架第 1 条的最后一环: "对失败归因")
2
+ // 框架原文: "对失败归因: 是知识不够、工具不会用、规划错、验证缺失, 还是记忆没复用"
3
+ // 本模块把这件事从"人工看日志"变成"从轨迹确定性判定", 零 LLM, 可复现, 可测。
4
+ //
5
+ // 输入: 一次任务的执行轨迹 (最终回复 + 工具调用序列 + 耗时 + 判分结果)
6
+ // 输出: { cause, confidence, evidence[], action } —— action 直接对应"按症状下药"表的处方
7
+
8
+ export const CAUSE = {
9
+ /** 卡在 LLM 侧: 单次调用悬挂, 任务时限先到 */
10
+ LLM_STALL: "llm_stall",
11
+ /** 卡在规划侧: 重复调用同一工具/参数, 循环不收敛 */
12
+ LOOP: "loop",
13
+ /** 跑满时限但轨迹不足以判定 */
14
+ TIMEOUT_UNKNOWN: "timeout_unknown",
15
+ /** 工具不会用: 参数错/未知工具/被禁用/权限拦截 */
16
+ TOOL_USE: "tool_use",
17
+ /** 规划错: 一直在试但没收敛 (轮次逼近上限且无错误) */
18
+ PLANNING: "planning",
19
+ /** 验证缺失: 工具都成功了、有产出, 但结果不对 —— 没做自检 */
20
+ VERIFICATION: "verification",
21
+ /** 知识不够: 几乎没查就作答 */
22
+ KNOWLEDGE: "knowledge",
23
+ UNKNOWN: "unknown",
24
+ };
25
+
26
+ // 每类根的处方 (对齐框架"按症状下药"表)
27
+ const PRESCRIPTION = {
28
+ [CAUSE.LLM_STALL]: "单次 LLM 超时 (provider.timeout_ms) 必须小于任务级时限; 并给工具循环加「心跳超时」",
29
+ [CAUSE.LOOP]: "任务分解 + 验证器 + 重规划; 检查探索熔断阈值是否过松",
30
+ [CAUSE.TIMEOUT_UNKNOWN]: "补全轨迹采集后重跑定位 (当前轨迹不足以归因)",
31
+ [CAUSE.TOOL_USE]: "结构化输出 + 参数校验 + 失败重试 + 沙箱; 优先修该工具的 schema 与示例",
32
+ [CAUSE.PLANNING]: "任务分解 + 验证器 + 重规划 (轮次逼近上限仍未收敛)",
33
+ [CAUSE.VERIFICATION]: "生成 → 验证 → 修正: 给这类任务配确定性校验器, 让 agent 自查后再作答",
34
+ [CAUSE.KNOWLEDGE]: "RAG + 引用核验 + 工具查询 (先查再答, 不要凭记忆作答)",
35
+ [CAUSE.UNKNOWN]: "采集更多轨迹样本后重判",
36
+ };
37
+
38
+ // 工具调用签名 (同工具同参数视为重复)
39
+ function sig(c) {
40
+ try {
41
+ return `${c.tool}::${JSON.stringify(c.args || {}).slice(0, 120)}`;
42
+ } catch {
43
+ return String(c.tool);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * 从轨迹归因一次失败
49
+ * @param {object} o
50
+ * @param {string} o.reply 最终回复
51
+ * @param {Array} o.toolCalls 工具调用序列 [{tool,args,ok,error,durationMs}]
52
+ * @param {number} o.ms 总耗时
53
+ * @param {number} o.budgetMs 任务时限 (默认 90000)
54
+ * @param {number} o.maxRounds 工具循环轮次上限
55
+ * @param {string} o.detail 判分说明
56
+ * @returns {{cause:string, confidence:number, evidence:string[], action:string}}
57
+ */
58
+ export function triageFailure({ reply = "", toolCalls = [], ms = 0, budgetMs = 90000, maxRounds = 8, detail = "" } = {}) {
59
+ const calls = Array.isArray(toolCalls) ? toolCalls : [];
60
+ const evidence = [];
61
+ const fail = (cause, confidence) => ({ cause, confidence, evidence, action: PRESCRIPTION[cause] });
62
+
63
+ const failedCalls = calls.filter((c) => c && c.ok === false);
64
+ const calledTools = new Set(calls.map((c) => c && c.tool).filter(Boolean));
65
+
66
+ // ---- 1. 超时族: 跑满时限才被砍 ----
67
+ const timedOut = ms >= budgetMs * 0.98 || /超时|timeout/i.test(String(reply || ""));
68
+ if (timedOut) {
69
+ evidence.push(`耗时 ${ms}ms 已达任务时限 ${budgetMs}ms`);
70
+ // 1a. 重复签名 → 规划不收敛
71
+ const counts = new Map();
72
+ for (const c of calls) {
73
+ const k = sig(c);
74
+ counts.set(k, (counts.get(k) || 0) + 1);
75
+ }
76
+ const worst = [...counts.entries()].sort((a, b) => b[1] - a[1])[0];
77
+ if (worst && worst[1] >= 3) {
78
+ evidence.push(`同一调用重复 ${worst[1]} 次: ${worst[0]}`);
79
+ return fail(CAUSE.LOOP, 0.85);
80
+ }
81
+ // 1b. 工具调用很少 → 时间花在 LLM 侧
82
+ if (calls.length <= 2) {
83
+ evidence.push(`整个任务只发生 ${calls.length} 次工具调用, 时间几乎全在 LLM 往返`);
84
+ return fail(CAUSE.LLM_STALL, 0.7);
85
+ }
86
+ evidence.push(`工具调用 ${calls.length} 次, 无重复签名, 但仍未收敛`);
87
+ return fail(CAUSE.TIMEOUT_UNKNOWN, 0.4);
88
+ }
89
+
90
+ // ---- 2. 工具不会用: 有工具返回错误 ----
91
+ if (failedCalls.length) {
92
+ const kinds = new Map();
93
+ for (const c of failedCalls) {
94
+ const e = String(c.error || "");
95
+ const k = /参数错误|应为|必填/.test(e) ? "参数校验被拦"
96
+ : /未知工具|已禁用/.test(e) ? "工具不可用"
97
+ : /策略拦截|权限|审批/.test(e) ? "权限/策略拦截"
98
+ : /超时/.test(e) ? "工具超时"
99
+ : "执行报错";
100
+ kinds.set(k, (kinds.get(k) || 0) + 1);
101
+ }
102
+ for (const [k, n] of kinds) evidence.push(`${k} × ${n} (涉及工具: ${[...new Set(failedCalls.map((c) => c.tool))].slice(0, 3).join(", ")})`);
103
+ return fail(CAUSE.TOOL_USE, 0.9);
104
+ }
105
+
106
+ // ---- 3. 规划错: 轮次逼近上限仍未收敛 ----
107
+ if (maxRounds > 0 && calls.length >= Math.max(3, Math.floor(maxRounds * 0.75))) {
108
+ evidence.push(`工具调用 ${calls.length} 次, 逼近轮次上限 ${maxRounds}, 无错误但未收敛`);
109
+ return fail(CAUSE.PLANNING, 0.7);
110
+ }
111
+
112
+ // ---- 4. 验证缺失: 该查的都查了、没错、有产出, 但结果不对 ----
113
+ if (calls.length >= 1) {
114
+ evidence.push(`工具调用 ${calls.length} 次且全部成功, 但判分失败 (${detail || "结果不符"})`);
115
+ evidence.push(`涉及工具: ${[...calledTools].join(", ")}`);
116
+ return fail(CAUSE.VERIFICATION, 0.75);
117
+ }
118
+
119
+ // ---- 5. 知识不够: 几乎没查就作答 ----
120
+ evidence.push(`工具调用 ${calls.length} 次 —— 基本没查就作答, 判分失败 (${detail || "结果不符"})`);
121
+ return fail(CAUSE.KNOWLEDGE, 0.8);
122
+ }
123
+
124
+ // 汇总一批归因结果 → 按根因分布 (供 self_diagnose / 报告展示)
125
+ export function summarizeCauses(triages = []) {
126
+ const byCause = {};
127
+ for (const t of triages) {
128
+ if (!t || !t.cause) continue;
129
+ byCause[t.cause] = (byCause[t.cause] || 0) + 1;
130
+ }
131
+ const top = Object.entries(byCause).sort((a, b) => b[1] - a[1])[0];
132
+ return {
133
+ total: triages.filter((t) => t && t.cause).length,
134
+ byCause,
135
+ dominant: top ? top[0] : null,
136
+ dominantAction: top ? PRESCRIPTION[top[0]] : null,
137
+ };
138
+ }
@@ -0,0 +1,76 @@
1
+ // src/session/parts.js - 结构化消息部件 (opencode 风格)
2
+ // 每个部件: { id, type, ...字段, ts }; id 用 crypto.randomUUID()
3
+ // - part.* 部件工厂
4
+ // - parts.* 含工厂 + fromResponseItems + estimateTokens
5
+ import { randomUUID } from "node:crypto";
6
+
7
+ function _base(type, extra) {
8
+ return { id: randomUUID(), type, ts: Date.now(), ...extra };
9
+ }
10
+
11
+ // 部件工厂
12
+ export const part = {
13
+ text(text) {
14
+ return _base("text", { text });
15
+ },
16
+ file({ path: p, content }) {
17
+ return _base("file", { path: p, content });
18
+ },
19
+ image({ url }) {
20
+ return _base("image", { url });
21
+ },
22
+ agent({ name, color }) {
23
+ return _base("agent", { name, color });
24
+ },
25
+ reasoning(text) {
26
+ return _base("reasoning", { text });
27
+ },
28
+ tool({ call, output }) {
29
+ return _base("tool", { call, output });
30
+ },
31
+ };
32
+
33
+ // 把 ResponseItem 数组映射为部件数组 (对齐 codex ResponseItem 形态)
34
+ export function fromResponseItems(items = []) {
35
+ const out = [];
36
+ for (const it of items) {
37
+ if (!it || !it.type) continue;
38
+ switch (it.type) {
39
+ case "message":
40
+ out.push(part.text(it.content ?? ""));
41
+ break;
42
+ case "reasoning":
43
+ out.push(part.reasoning(it.text ?? ""));
44
+ break;
45
+ case "function_call":
46
+ out.push(part.tool({ call: { name: it.name, arguments: it.arguments }, output: null }));
47
+ break;
48
+ case "function_call_output":
49
+ out.push(part.tool({ call: null, output: it.output }));
50
+ break;
51
+ default:
52
+ // 未知类型: 兜底为文本部件, 不丢信息
53
+ out.push(part.text(JSON.stringify(it)));
54
+ }
55
+ }
56
+ return out;
57
+ }
58
+
59
+ // 粗估 token 数: 中文每字约 1.6 字符/token, 其它非空白字符约 3.5 字符/token, 分段累加后向上取整
60
+ export function estimateTokens(text) {
61
+ if (!text) return 0;
62
+ const s = String(text);
63
+ let cn = 0, other = 0;
64
+ for (const ch of s) {
65
+ if (/[㐀-䶿一-鿿]/.test(ch)) cn++; // CJK 基本区 + 扩展A
66
+ else if (!/\s/.test(ch)) other++;
67
+ }
68
+ return Math.ceil(cn / 1.6 + other / 3.5);
69
+ }
70
+
71
+ // 对外命名空间: 含全部工厂 + 转换/估算能力
72
+ export const parts = {
73
+ ...part,
74
+ fromResponseItems,
75
+ estimateTokens,
76
+ };
@@ -0,0 +1,73 @@
1
+ // src/session/projection.js - Turn 投影层 (v3.1 首片集成, 2026-10-03)
2
+ //
3
+ // 定位: src/session/turn.js 的 Session→Task→Turn 状态机作为**投影层**接入主链路,
4
+ // SessionStore (data/sessions/*.jsonl) 仍是会话内容的唯一事实源。
5
+ // 本层只记录「每轮对话的生命周期状态」(open/closed/aborted), 不参与消息路由,
6
+ // 对 chat/chatStream 是纯可观测增量 —— 后续 v3.1 排队输入 (queuedUserInputs)
7
+ // 承接、rollout 持久化都在这层之上演进, 不必再动主链路。
8
+ //
9
+ // 事件: agent.bus 发 chat/turn/begin、chat/turn/end (status: closed|aborted);
10
+ // protocolBus.eq 有 SQ/EQ 总线时同步推 TASK_TURN_* 结构化事件。
11
+ import { createSession } from "./turn.js";
12
+
13
+ export class TurnProjection {
14
+ constructor() {
15
+ this.sessions = new Map(); // sessionKey -> session 状态机实例 (内存投影, 轻量)
16
+ this.enabled = true; // config.agent.turn_projection === false 时整体旁路
17
+ }
18
+
19
+ _get(sessionKey) {
20
+ let s = this.sessions.get(sessionKey);
21
+ if (!s) {
22
+ s = createSession({ id: sessionKey });
23
+ this.sessions.set(sessionKey, s);
24
+ }
25
+ return s;
26
+ }
27
+
28
+ // 开始一轮: 开新 turn 承接用户输入
29
+ begin(sessionKey, userMsg) {
30
+ if (!this.enabled) return null;
31
+ try {
32
+ const s = this._get(sessionKey);
33
+ const task = s.currentTask() || s.createTask();
34
+ const turn = task.appendTurn(String(userMsg ?? ""));
35
+ return { sessionId: s.id, turnId: turn.id, seq: turn.seq };
36
+ } catch { return null; } // 投影层永不影响主链路
37
+ }
38
+
39
+ // 正常结束一轮 (排队输入存在时状态机自动开下一个 turn, 本层暂不消费)
40
+ complete(sessionKey) {
41
+ if (!this.enabled) return null;
42
+ try {
43
+ const s = this.sessions.get(sessionKey);
44
+ const task = s && s.currentTask();
45
+ const turn = task && task.completeTurn();
46
+ return turn ? { turnId: turn.id, status: turn.status, queued: task.queuedUserInputs.length } : null;
47
+ } catch { return null; }
48
+ }
49
+
50
+ // 中止一轮 (中断/异常)
51
+ abort(sessionKey) {
52
+ if (!this.enabled) return null;
53
+ try {
54
+ const s = this.sessions.get(sessionKey);
55
+ const task = s && s.currentTask();
56
+ return task ? { turnId: task.currentTurn?.id, status: "aborted" } : null;
57
+ } catch { return null; }
58
+ }
59
+
60
+ // 诊断视图: 指定会话的 turn 概要
61
+ status(sessionKey) {
62
+ const s = this.sessions.get(sessionKey);
63
+ if (!s) return null;
64
+ const task = s.currentTask();
65
+ return {
66
+ sessionId: s.id,
67
+ turnCount: s.turnCount,
68
+ taskStatus: task ? task.status : null,
69
+ queuedInputs: task ? task.queuedUserInputs.length : 0,
70
+ turns: (task ? task.turns : []).slice(-10).map((t) => ({ id: t.id, seq: t.seq, status: t.status })),
71
+ };
72
+ }
73
+ }